Skip to content

Commit fbe8e50

Browse files
committed
Add CI integration for Spark-X2.5 models
This commit adds CI test support for Spark-X2.5-1.7B and Spark-X2.5-4B models to the ExecuTorch test infrastructure. Changes: - Added spark_x2_5_1_7b and spark_x2_5_4b to test_torchao_huggingface_checkpoints.sh - Added both models to CI test matrix in trunk.yml - Configured XNNPACK (x86_64) and TorchAO (ARM64) backend testing - Use --local-dir for hf download to avoid read-only /mnt/hf_cache issues - convert_weights now rewrites the no-op normalizer in Spark-X2.5 tokenizer.json (empty Sequence, rejected by older revisions of the C++ HF tokenizer) to null, which is semantically identical, so llama_main can load it. The root cause is also fixed upstream in meta-pytorch/tokenizers#225; the submodule pin will be bumped once that lands. The Spark-X2.5 model infrastructure already exists in the codebase. This commit completes the integration by adding automated CI testing. Parts of this commit were authored with AI assistance using Claude Code. Signed-off-by: dongjiang <dongjiang1989@126.com>
1 parent 1169410 commit fbe8e50

4 files changed

Lines changed: 120 additions & 5 deletions

File tree

‎.ci/scripts/test_torchao_huggingface_checkpoints.sh‎

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ MODEL_NAME=""
1111
# Parse args
1212
if [[ $# -lt 1 ]]; then
1313
echo "Usage: $0 <model_name> [--test_with_runner]"
14-
echo "Supported model_name values: qwen3_4b, phi_4_mini, lfm2_5_1_2b"
14+
echo "Supported model_name values: qwen3_4b, phi_4_mini, lfm2_5_1_2b, spark_x2_5_1_7b, spark_x2_5_4b"
1515
exit 1
1616
fi
1717

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

128+
spark_x2_5_1_7b)
129+
echo "Running Spark-X2.5-1.7B export..."
130+
HF_MODEL_DIR="${RUNNER_TEMP:-/tmp}/hf_cache/spark_x2_5_1_7b"
131+
mkdir -p "${HF_MODEL_DIR}"
132+
hf download --local-dir "${HF_MODEL_DIR}" XHToken/Spark-X2.5-1.7B
133+
EXPECTED_MODEL_SIZE_UPPER_BOUND=$((2 * 1024 * 1024 * 1024)) # 2GB
134+
$PYTHON_EXECUTABLE -m executorch.examples.models.spark_x2_5.convert_weights \
135+
$HF_MODEL_DIR \
136+
pytorch_model_converted.bin
137+
138+
$PYTHON_EXECUTABLE -m executorch.examples.models.llama.export_llama \
139+
--model "spark_x2_5_1_7b" \
140+
--checkpoint pytorch_model_converted.bin \
141+
--params examples/models/spark_x2_5/config/spark_x2_5_1_7b_config.json \
142+
--output_name $MODEL_OUT \
143+
-kv \
144+
--use_sdpa_with_kv_cache \
145+
--max_context_length 1024 \
146+
--max_seq_length 1024 \
147+
--metadata '{"get_bos_id":0, "get_eos_ids":[1]}' \
148+
--verbose \
149+
--dtype fp32 \
150+
-qmode 8da4w \
151+
${BACKEND_ARGS}
152+
;;
153+
154+
spark_x2_5_4b)
155+
echo "Running Spark-X2.5-4B export..."
156+
HF_MODEL_DIR="${RUNNER_TEMP:-/tmp}/hf_cache/spark_x2_5_4b"
157+
mkdir -p "${HF_MODEL_DIR}"
158+
hf download --local-dir "${HF_MODEL_DIR}" XHToken/Spark-X2.5-4B
159+
EXPECTED_MODEL_SIZE_UPPER_BOUND=$((5 * 1024 * 1024 * 1024)) # 5GB
160+
$PYTHON_EXECUTABLE -m executorch.examples.models.spark_x2_5.convert_weights \
161+
$HF_MODEL_DIR \
162+
pytorch_model_converted.bin
163+
164+
$PYTHON_EXECUTABLE -m executorch.examples.models.llama.export_llama \
165+
--model "spark_x2_5_4b" \
166+
--checkpoint pytorch_model_converted.bin \
167+
--params examples/models/spark_x2_5/config/spark_x2_5_4b_config.json \
168+
--output_name $MODEL_OUT \
169+
-kv \
170+
--use_sdpa_with_kv_cache \
171+
--max_context_length 1024 \
172+
--max_seq_length 1024 \
173+
--metadata '{"get_bos_id":0, "get_eos_ids":[1]}' \
174+
--verbose \
175+
--dtype fp32 \
176+
-qmode 8da4w \
177+
${BACKEND_ARGS}
178+
;;
179+
128180
*)
129181
echo "Error: unsupported model_name '$MODEL_NAME'"
130-
echo "Supported values: qwen3_4b, phi_4_mini, lfm2_5_1_2b"
182+
echo "Supported values: qwen3_4b, phi_4_mini, lfm2_5_1_2b, spark_x2_5_1_7b, spark_x2_5_4b"
131183
exit 1
132184
;;
133185
esac

‎.github/workflows/trunk.yml‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ jobs:
576576
contents: read
577577
strategy:
578578
matrix:
579-
model: [qwen3_4b, phi_4_mini, lfm2_5_1_2b]
579+
model: [qwen3_4b, phi_4_mini, lfm2_5_1_2b, spark_x2_5_1_7b, spark_x2_5_4b]
580580
runner: [mt-l-x86iavx512-8-64]
581581
docker-image: [executorch-ubuntu-22.04-clang12]
582582
backend: [xnnpack]
@@ -593,6 +593,14 @@ jobs:
593593
runner: mt-l-arm64g4-16-62
594594
docker-image: executorch-ubuntu-22.04-gcc11-aarch64
595595
backend: torchao
596+
- model: spark_x2_5_1_7b
597+
runner: mt-l-arm64g4-16-62
598+
docker-image: executorch-ubuntu-22.04-gcc11-aarch64
599+
backend: torchao
600+
- model: spark_x2_5_4b
601+
runner: mt-l-arm64g4-16-62
602+
docker-image: executorch-ubuntu-22.04-gcc11-aarch64
603+
backend: torchao
596604
fail-fast: false
597605
with:
598606
runner: ${{ matrix.runner }}

‎examples/models/spark_x2_5/convert_weights.py‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from typing import Dict
55

66
import torch
7-
87
from executorch.examples.models.checkpoint import get_mapped_key
98
from safetensors.torch import load_file
109

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

9493

94+
def sanitize_tokenizer(input_dir: str) -> None:
95+
"""Make tokenizer.json loadable by the C++ HF tokenizer.
96+
97+
Spark-X2.5 ships a no-op normalizer ({"type": "Sequence", "normalizers":
98+
[]}) that the C++ tokenizer rejects; a null normalizer is equivalent and
99+
accepted.
100+
"""
101+
path = os.path.join(input_dir, "tokenizer.json")
102+
if not os.path.exists(path):
103+
return
104+
with open(path) as f:
105+
tokenizer = json.load(f)
106+
normalizer = tokenizer.get("normalizer")
107+
if (
108+
isinstance(normalizer, dict)
109+
and normalizer.get("type") == "Sequence"
110+
and not normalizer.get("normalizers")
111+
):
112+
tokenizer["normalizer"] = None
113+
with open(path, "w") as f:
114+
json.dump(tokenizer, f)
115+
print("Replaced no-op normalizer in tokenizer.json with null")
116+
117+
95118
def convert_weights(input_dir: str, output_file: str) -> None:
96119
"""Convert Spark-X2.5 HF weights to Meta format.
97120
98121
Reads model hyperparameters from config.json in input_dir so the function
99122
signature stays compatible with download_and_convert_hf_checkpoint.
100123
"""
124+
sanitize_tokenizer(input_dir)
101125
config_path = os.path.join(input_dir, "config.json")
102126
if os.path.exists(config_path):
103127
with open(config_path) as f:

‎examples/models/spark_x2_5/test_spark_x2_5.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,37 @@ def test_spark_x2_5_architecture_configs_match_expected_shapes() -> None:
137137
)
138138

139139

140+
def test_sanitize_tokenizer_replaces_noop_normalizer(tmp_path) -> None:
141+
from executorch.examples.models.spark_x2_5.convert_weights import sanitize_tokenizer
142+
143+
tokenizer = {
144+
"normalizer": {"type": "Sequence", "normalizers": []},
145+
"model": {"type": "BPE"},
146+
}
147+
tokenizer_path = tmp_path / "tokenizer.json"
148+
tokenizer_path.write_text(json.dumps(tokenizer))
149+
150+
sanitize_tokenizer(str(tmp_path))
151+
152+
sanitized = json.loads(tokenizer_path.read_text())
153+
assert sanitized["normalizer"] is None
154+
assert sanitized["model"] == {"type": "BPE"}
155+
156+
157+
def test_sanitize_tokenizer_keeps_meaningful_normalizer(tmp_path) -> None:
158+
from executorch.examples.models.spark_x2_5.convert_weights import sanitize_tokenizer
159+
160+
normalizer = {"type": "Sequence", "normalizers": [{"type": "NFC"}]}
161+
tokenizer = {"normalizer": normalizer, "model": {"type": "BPE"}}
162+
tokenizer_path = tmp_path / "tokenizer.json"
163+
tokenizer_path.write_text(json.dumps(tokenizer))
164+
165+
sanitize_tokenizer(str(tmp_path))
166+
167+
sanitized = json.loads(tokenizer_path.read_text())
168+
assert sanitized["normalizer"] == normalizer
169+
170+
140171
def test_spark_x2_5_xnnpack_q8da4w_config_enables_xnnpack_backend() -> None:
141172
cfg = OmegaConf.load(CONFIG_DIR / "spark_x2_5_xnnpack_q8da4w.yaml")
142173
assert cfg.base.metadata == '{"get_bos_id": 0, "get_eos_ids":[1]}'

0 commit comments

Comments
 (0)