Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions .ci/scripts/test_torchao_huggingface_checkpoints.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ MODEL_NAME=""
# Parse args
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <model_name> [--test_with_runner]"
echo "Supported model_name values: qwen3_4b, phi_4_mini, lfm2_5_1_2b"
echo "Supported model_name values: qwen3_4b, phi_4_mini, lfm2_5_1_2b, spark_x2_5_1_7b, spark_x2_5_4b"
exit 1
fi

Expand All @@ -28,7 +28,7 @@ while [[ $# -gt 0 ]]; do
;;
-h|--help)
echo "Usage: $0 <model_name> [--test_with_runner] [--use_torchao_kernels]"
echo " model_name: qwen3_4b | phi_4_mini | lfm2_5_1_2b"
echo " model_name: qwen3_4b | phi_4_mini | lfm2_5_1_2b | spark_x2_5_1_7b | spark_x2_5_4b"
echo " --test_with_runner: build ET + run llama_main to sanity-check the export"
echo " --use_torchao_kernels: use torchao kernels for linear and tied embedding"
exit 0
Expand Down Expand Up @@ -125,9 +125,61 @@ case "$MODEL_NAME" in
${BACKEND_ARGS}
;;

spark_x2_5_1_7b)
echo "Running Spark-X2.5-1.7B export..."
HF_MODEL_DIR="${RUNNER_TEMP:-/tmp}/hf_cache/spark_x2_5_1_7b"
mkdir -p "${HF_MODEL_DIR}"
hf download --local-dir "${HF_MODEL_DIR}" XHToken/Spark-X2.5-1.7B
EXPECTED_MODEL_SIZE_UPPER_BOUND=$((2 * 1024 * 1024 * 1024)) # 2GB
$PYTHON_EXECUTABLE -m executorch.examples.models.spark_x2_5.convert_weights \
$HF_MODEL_DIR \
pytorch_model_converted.bin

$PYTHON_EXECUTABLE -m executorch.examples.models.llama.export_llama \
--model "spark_x2_5_1_7b" \
--checkpoint pytorch_model_converted.bin \
--params examples/models/spark_x2_5/config/spark_x2_5_1_7b_config.json \
--output_name $MODEL_OUT \
-kv \
--use_sdpa_with_kv_cache \
--max_context_length 1024 \
--max_seq_length 1024 \
--metadata '{"get_bos_id":0, "get_eos_ids":[1]}' \
--verbose \
--dtype fp32 \
-qmode 8da4w \
${BACKEND_ARGS}
;;

spark_x2_5_4b)
echo "Running Spark-X2.5-4B export..."
HF_MODEL_DIR="${RUNNER_TEMP:-/tmp}/hf_cache/spark_x2_5_4b"
mkdir -p "${HF_MODEL_DIR}"
hf download --local-dir "${HF_MODEL_DIR}" XHToken/Spark-X2.5-4B
EXPECTED_MODEL_SIZE_UPPER_BOUND=$((5 * 1024 * 1024 * 1024)) # 5GB
$PYTHON_EXECUTABLE -m executorch.examples.models.spark_x2_5.convert_weights \
$HF_MODEL_DIR \
pytorch_model_converted.bin

$PYTHON_EXECUTABLE -m executorch.examples.models.llama.export_llama \
--model "spark_x2_5_4b" \
--checkpoint pytorch_model_converted.bin \
--params examples/models/spark_x2_5/config/spark_x2_5_4b_config.json \
--output_name $MODEL_OUT \
-kv \
--use_sdpa_with_kv_cache \
--max_context_length 1024 \
--max_seq_length 1024 \
--metadata '{"get_bos_id":0, "get_eos_ids":[1]}' \
--verbose \
--dtype fp32 \
-qmode 8da4w \
${BACKEND_ARGS}
;;

*)
echo "Error: unsupported model_name '$MODEL_NAME'"
echo "Supported values: qwen3_4b, phi_4_mini, lfm2_5_1_2b"
echo "Supported values: qwen3_4b, phi_4_mini, lfm2_5_1_2b, spark_x2_5_1_7b, spark_x2_5_4b"
exit 1
;;
esac
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/trunk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ jobs:
contents: read
strategy:
matrix:
model: [qwen3_4b, phi_4_mini, lfm2_5_1_2b]
model: [qwen3_4b, phi_4_mini, lfm2_5_1_2b, spark_x2_5_1_7b, spark_x2_5_4b]
runner: [mt-l-x86iavx512-8-64]
docker-image: [executorch-ubuntu-22.04-clang12]
backend: [xnnpack]
Expand All @@ -593,6 +593,14 @@ jobs:
runner: mt-l-arm64g4-16-62
docker-image: executorch-ubuntu-22.04-gcc11-aarch64
backend: torchao
- model: spark_x2_5_1_7b
runner: mt-l-arm64g4-16-62
docker-image: executorch-ubuntu-22.04-gcc11-aarch64
backend: torchao
- model: spark_x2_5_4b
runner: mt-l-arm64g4-16-62
docker-image: executorch-ubuntu-22.04-gcc11-aarch64
backend: torchao
fail-fast: false
with:
runner: ${{ matrix.runner }}
Expand Down
26 changes: 25 additions & 1 deletion examples/models/spark_x2_5/convert_weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from typing import Dict

import torch

from executorch.examples.models.checkpoint import get_mapped_key
from safetensors.torch import load_file

Expand Down Expand Up @@ -92,12 +91,37 @@ def load_checkpoint(input_dir: str) -> Dict[str, torch.Tensor]:
raise FileNotFoundError(f"Could not find safetensors checkpoint in {input_dir}")


def sanitize_tokenizer(input_dir: str) -> None:
"""Make tokenizer.json loadable by the C++ HF tokenizer.

Spark-X2.5 ships a no-op normalizer ({"type": "Sequence", "normalizers":
[]}) that the C++ tokenizer rejects; a null normalizer is equivalent and
accepted.
"""
path = os.path.join(input_dir, "tokenizer.json")
if not os.path.exists(path):
return
with open(path) as f:
tokenizer = json.load(f)
normalizer = tokenizer.get("normalizer")
if (
isinstance(normalizer, dict)
and normalizer.get("type") == "Sequence"
and not normalizer.get("normalizers")
):
tokenizer["normalizer"] = None
with open(path, "w") as f:
json.dump(tokenizer, f)
print("Replaced no-op normalizer in tokenizer.json with null")


def convert_weights(input_dir: str, output_file: str) -> None:
"""Convert Spark-X2.5 HF weights to Meta format.

Reads model hyperparameters from config.json in input_dir so the function
signature stays compatible with download_and_convert_hf_checkpoint.
"""
sanitize_tokenizer(input_dir)
config_path = os.path.join(input_dir, "config.json")
if os.path.exists(config_path):
with open(config_path) as f:
Expand Down
31 changes: 31 additions & 0 deletions examples/models/spark_x2_5/test_spark_x2_5.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,37 @@ def test_spark_x2_5_architecture_configs_match_expected_shapes() -> None:
)


def test_sanitize_tokenizer_replaces_noop_normalizer(tmp_path) -> None:
from executorch.examples.models.spark_x2_5.convert_weights import sanitize_tokenizer

tokenizer = {
"normalizer": {"type": "Sequence", "normalizers": []},
"model": {"type": "BPE"},
}
tokenizer_path = tmp_path / "tokenizer.json"
tokenizer_path.write_text(json.dumps(tokenizer))

sanitize_tokenizer(str(tmp_path))

sanitized = json.loads(tokenizer_path.read_text())
assert sanitized["normalizer"] is None
assert sanitized["model"] == {"type": "BPE"}


def test_sanitize_tokenizer_keeps_meaningful_normalizer(tmp_path) -> None:
from executorch.examples.models.spark_x2_5.convert_weights import sanitize_tokenizer

normalizer = {"type": "Sequence", "normalizers": [{"type": "NFC"}]}
tokenizer = {"normalizer": normalizer, "model": {"type": "BPE"}}
tokenizer_path = tmp_path / "tokenizer.json"
tokenizer_path.write_text(json.dumps(tokenizer))

sanitize_tokenizer(str(tmp_path))

sanitized = json.loads(tokenizer_path.read_text())
assert sanitized["normalizer"] == normalizer


def test_spark_x2_5_xnnpack_q8da4w_config_enables_xnnpack_backend() -> None:
cfg = OmegaConf.load(CONFIG_DIR / "spark_x2_5_xnnpack_q8da4w.yaml")
assert cfg.base.metadata == '{"get_bos_id": 0, "get_eos_ids":[1]}'
Expand Down
Loading