Repository navigation
chore: bench_decode.sh all measures duplicate checkpoints twice #1615
Description
Body rewritten 2026-09-04. The body originally filed here was a copy of #1616 (batched MoE decode scaling) and did not describe this issue at all; the two differed only in cosmetic wording. Everything below is reconstructed from the script and from the committed benchmark CSVs. The title was correct.
Problem / Background
bench_decode.sh all enumerates "$MODELS_DIR"/*/ and benchmarks every directory it finds (scripts/bench_decode.sh:658 and :666). Nothing deduplicates, so a checkpoint present under two directory names is loaded, prefilled, decoded and cooled down twice, and lands in the CSV as two rows under two names. Downstream the duplicate rows are treated as two distinct models.
Evidence
Scanning models/mlx on M1 Ultra by sha256(config.json) plus total *.safetensors bytes finds 15 duplicate groups covering 16 redundant directories out of 199, holding 124.5 GB of redundant weights.
| Group | Copies | Weights each |
|---|---|---|
hunyuan-13b / hunyuan-a13b-instruct-4bit |
2 | 45.22 GB |
nemotron-h-30b-4bit / nemotron-nas-30b-4bit |
2 | 17.78 GB |
qwen3-30b-a3b-4bit / qwen3-moe-4bit |
2 | 17.17 GB |
qwen2.5-vl-3b-hf / qwen2.5-vl-3b-instruct |
2 | 7.51 GB |
pixtral-12b / pixtral-12b-4bit |
2 | 7.14 GB |
qwen3-vl-8b-4bit / qwen3-vl-8b-instruct-4bit |
2 | 5.76 GB |
llama-3.1-8b-4bit / meta-llama-3.1-8b-instruct-4bit |
2 | 4.52 GB |
qwen2.5-7b / qwen2.5-7b-4bit / qwen2.5-7b-instruct-4bit |
3 | 4.28 GB |
gemma-3-4b-it-4bit / gemma3-4b-4bit |
2 | 3.40 GB |
qwen3-vl-4b-4bit / qwen3-vl-4b-instruct-4bit |
2 | 3.09 GB |
qwen3-vl-2b / qwen3-vl-2b-4bit |
2 | 1.78 GB |
qwen2-vl-2b / qwen2-vl-2b-4bit |
2 | 1.25 GB |
gemma-3-1b-it-4bit / gemma3-1b-4bit |
2 | 0.73 GB |
qwen3-0.6b / qwen3-0.6b-4bit |
2 | 0.34 GB |
qwen2-0.5b / qwen2.5-0.5b-4bit |
2 | 0.28 GB |
Six groups were additionally spot-checked by shard count plus a sha256 over the first and last 1 MiB of every shard: all six match, and none share inodes, so these are independent full copies rather than hardlinks or APFS clones. Names are misleading in both directions: pixtral-12b and pixtral-12b-4bit are the same 4-bit checkpoint, and hunyuan-13b is the same file set as hunyuan-a13b-instruct-4bit.
The duplication is in every committed sweep, not just on this host. Counting group members that co-occur in each committed CSV:
| Sweep | Rows | Redundant rows |
|---|---|---|
metal_m1ultra_2026-07-12.csv |
168 | 15 |
metal_m1ultra_2026-07-06.csv |
169 | 14 |
metal_m1ultra_2026-06-15.csv |
151 | 13 |
metal_m5max_2026-07-12.csv |
176 | 14 |
metal_m5max_2026-06-15.csv |
151 | 13 |
metal_m5max_2026-04-04.csv |
95 | 2 |
The paired numbers agree to within run noise, which is itself confirmation that the two rows measure one checkpoint: qwen2.5-7b / qwen2.5-7b-4bit / qwen2.5-7b-instruct-4bit decode at 108.85 / 108.47 / 108.86 tok/s in metal_m1ultra_2026-07-12.csv, and nemotron-h-30b-4bit / nemotron-nas-30b-4bit at 91.75 / 90.37. The VLM sweeps carry the same redundancy at the same counts.
It reaches published documentation. Three duplicate groups appear as separate table rows in each of docs/benchmark_results/model_tests_m1ultra.md and docs/benchmark_results/model_tests_m5max.md (gemma-3-4b-it-4bit / gemma3-4b-4bit, nemotron-h-30b-4bit / nemotron-nas-30b-4bit, pixtral-12b / pixtral-12b-4bit, qwen3-30b-a3b-4bit / qwen3-moe-4bit depending on the file), so one checkpoint is presented as two supported models. The parity comparison tables in the same documents list only one member per group, which means the deduplication currently exists only as unrecorded manual curation between the CSV and the document. That is not reproducible and it silently drifts every time someone regenerates a table.
Current Behavior
if [[ "$MODEL_ARG" == "all" ]]; then
# First pass: run all models except known GPU-crash models
for dir in "$MODELS_DIR"/*/; do
[[ -d "$dir" ]] || continue
is_gpu_crash_model "$dir" && continue
result=$(bench_one "$dir")The two-pass split at :658 and :666 exists to defer GPU_CRASH_MODELS (:603) to the end; each directory is still visited exactly once per sweep. The defect is that two directories holding one checkpoint are two directories. bench_one (:414) keys everything on the directory basename, and emit (:595) appends whatever it returns, so the CSV inherits the directory naming with no identity notion at all.
Proposed Solution
Deduplicate by checkpoint identity in all mode, before the first pass, and keep single-model mode untouched so an explicit path always measures exactly what was named.
- Compute a cheap identity key per candidate directory:
sha256(config.json)plus the sorted list of(*.safetensors basename, size). This costs one small read plus astatper shard, needs no weight hashing, and correctly separates checkpoints that differ only in quantization (theirconfig.jsondiffers in thequantizationblock). - Also collapse directories that resolve to the same path, so a symlink such as
models/mlx/gemma-3-1b-it-4bitinto the shared model store never doubles a row. - Measure one member per group and skip the rest, emitting one row per skipped alias with a
SKIP:duplicate_of=<name>status so the CSV stays self-describing and a reader can see the alias set rather than silently wondering where a model went. Row count then stays stable across sweeps, which matters becausedocs/benchmark_results/model_tests.mdindexes sweeps by measured row count. The survivor only has to be deterministic; as implemented it is the first directory the sweep's own glob reaches, which means the longer name wins for a prefix pair (pixtral-12b-4bitoverpixtral-12b). - Print the collapsed groups to stderr at sweep start so the operator sees what was folded before a multi-hour run begins.
- Add
--no-dedupto restore today's behavior for anyone who deliberately wants to measure two copies, for example when checking whether a re-download is bit-identical.
Rejected: deleting the redundant directories. models/ is gitignored local state, the aliases are how various scripts and documents already refer to these checkpoints (scripts/bench_embeddings.py, the deferred-pairing tables), and a sweep must not be silently wrong on a host that happens to hold two copies. The harness has to be correct regardless of how the model store is arranged.
Scope
In scope: scripts/bench_decode.sh (all mode enumeration, the new dedup helper, --no-dedup, usage text at :285), and a note in docs/benchmarks.md describing the identity key and the SKIP:duplicate_of status.
Out of scope: deleting or renaming any directory under models/; regenerating past sweeps; correcting the duplicate rows already present in committed CSVs and in model_tests_m1ultra.md and model_tests_m5max.md, which is a documentation pass that should follow once sweeps stop producing them; scripts/bench_embeddings.py and scripts/bench_serving_concurrency.py, which take explicit model lists rather than enumerating a directory.
Acceptance Criteria
-
bench_decode.sh allmeasures each distinct checkpoint exactly once, with the alias names emitted asSKIP:duplicate_of=<name>rows so total row count is unchanged. - The identity key does not collapse checkpoints that differ only in quantization or in weight dtype; a test or a documented check demonstrates this on a real pair from
models/mlx, for examplebitnet-b1.58-2b-4tagainstbitnet-b1.58-2b-4t-4bit. - A directory that is a symlink to another measured directory is collapsed as well.
-
--no-dedupreproduces the current behavior exactly. - Single-model mode (
bench_decode.sh models/<name>) is unaffected and still measures the named path. - The collapsed groups are printed to stderr before the first measurement.
-
docs/benchmarks.mddocuments the identity key and the new status string.
Verification
# Sanity-check the script and see the grouping the sweep would apply. The
# collapsed groups print to stderr before the first model is measured, so a
# sweep against a small temporary MODELS_DIR shows them without a long run.
bash -n scripts/bench_decode.sh
# Cheap end-to-end check: build a temporary MODELS_DIR of symlinks holding two
# small duplicate groups (gemma-3-1b-it-4bit / gemma3-1b-4bit and qwen2-0.5b /
# qwen2.5-0.5b-4bit) plus one non-duplicate control, then run both arms.
MODELS_DIR=<tmpdir> ./scripts/bench_decode.sh all --output /tmp/dedup.csv
MODELS_DIR=<tmpdir> ./scripts/bench_decode.sh all --no-dedup --output /tmp/nodedup.csvA pass is: one measured row per distinct checkpoint, the alias rows present and labelled, the sweep's total row count equal to the directory count so the CSV index in docs/benchmark_results/model_tests.md stays meaningful, and --no-dedup measuring every directory. There is no --dry-run; an earlier draft of this body referenced one that was never implemented, and the stderr group report plus a temporary MODELS_DIR cover the same need.
Technical Considerations
Relevant to #1614, which compares per-checkpoint prefill across two sweeps: duplicate rows make a "checkpoint" ambiguous in exactly the comparison that issue performs. Filed alongside #1612, #1613, #1614 and #1616 from the same 2026-09-04 benchmark campaign.
Activity
fix: dedup checkpoint aliases in bench_decode.sh all mode
inureyes commentedon Sep 4, 2026
Implemented in PR #1617, commit
5e810adf8This landed inside the open benchmark-refresh PR rather than as its own PR, because that branch already rewrites the same
bench_decode.shemit path: it renamesmlx_versiontomlxcel_versionand appendsmlxcel_commitandmlx_commit, taking the row from 15 fields to 17. A separate PR would have emitted the alias row in the old shape and conflicted on every touched line. #1617's body now carriesCloses #1615.What shipped.
allmode computes a checkpoint identity key before the first model runs:sha256(config.json)plus the sorted(basename, byte size)list of every*.safetensorsshard, with no weight hashing. Directories resolving to the same physical path collapse first, so a symlink into a shared model store never doubles a row; a directory with noconfig.jsonis never grouped by content and is always measured. The survivor is the first directory the sweep's own glob reaches, and every other member is emitted as its own row with the trailing statusSKIP:duplicate_of=<survivor>and no cooldown, so total row count is unchanged.--no-deduprestores the previous behavior exactly and single-model mode is untouched. The new status token is documented in the script header, in--help, and indocs/benchmarks.md.Verification. The grouping helper was run against
MODELS_DIR=models/mlxand independently re-run by the orchestrator: both found the same 15 groups and 16 redundant directories in the evidence table above, from 202 enumerated directories. The quantization-variant guard was confirmed onbitnet-b1.58-2b-4tagainstbitnet-b1.58-2b-4t-4bit, whoseconfig.jsonhashes differ. A realallsweep was run against a temporaryMODELS_DIRof symlinks covering two small duplicate groups plus one control: the dedup run produced two measured rows, twoSKIP:duplicate_of=rows and the control, and--no-dedupon the same directory measured all five.bash -nandshellcheckare clean. No cargo command was run because nothing undersrc/changed.One behavior worth knowing. The survivor is chosen by the glob's own sort over paths that carry a trailing slash, so where one name is a strict prefix of another the longer name wins:
pixtral-12b-4bitsurvives overpixtral-12b, andqwen2.5-7b-4bitover bothqwen2.5-7bandqwen2.5-7b-instruct-4bit. That is deterministic and stable, and it happens to keep the names the published tables already use (docs/benchmark_results/model_tests_m1ultra.mdlistsllama-3.1-8b-4bitandqwen2.5-7b-4bit), so no existing table row changes name.Not done here, deliberately. The duplicate rows already present in committed CSVs and the three duplicate groups appearing as separate table rows in
model_tests_m1ultra.mdandmodel_tests_m5max.mdare untouched. That documentation pass should follow once sweeps stop producing duplicates, and it is now mechanical: the alias table maintained by hand inmodel_tests_m5max.mdis derivable from a dedup-enabled sweep.No technical report was written under this issue's number. The work sits in #1617, which will carry one report for the whole sweep; the convention in
TECHNICAL_REPORTS/is one report per PR number and every existing number has exactly one.docs(bench): correct the stated dedup survivor rule
fix(bench): measure every model over the same interval
perf(cuda): convert bf16 weights to f16 at load on pre-Ampere GPUs (#…
perf(cuda/quant): add a Volta tensor-core arm to qmm_naive (#1678)
docs(benchmarks): refresh M5 Max results for 0.6.0
achimnol commentedon Oct 20, 2026
Closing as completed. The implementation described in the comment above shipped in #1617 (merged 2026-09-08), but that PR's body did not register a closing reference, so this issue stayed open.
Re-checked against
mainat 287a676:scripts/bench_decode.shcomputes the identity key fromsha256(config.json)plus the sorted(basename, byte size)of every*.safetensorsshard, and collapses directories that resolve to the same path viarealpathfirst.SKIP:duplicate_of=<survivor>rows, so the row count still equals the directory count, and the collapsed groups print to stderr before the first measurement.--no-dedupis documented in--helpand restores the previous behavior; single-model mode never dedups.docs/benchmarks.mddocuments the identity key, the survivor rule and the new status string.bash -n scripts/bench_decode.shpasses.The follow-up documentation pass for duplicate rows already in committed CSVs and in
model_tests_m1ultra.md/model_tests_m5max.mdremains out of scope, as the issue states.