Repository navigation
convert/model: store Zamba2's shared transformer block once per block (engine #1036) - #203
Conversation
… (engine ggml-org#1036) Every hybrid layer of a Zamba2 model runs the same transformer block, and the checkpoint stores it only num_mem_blocks times (twice at 7B), but the converter wrote one copy per hybrid layer -- 13 copies of two blocks, which is why our 7B BF16 is 10.977 B params / 20.45 GiB for a 7.357 B checkpoint. Write each block once, at its source layer, and alias the other layers to it in the loader. - conversion/zamba2.py: a block tail that this checkpoint stores no per-layer adapter for is written once per block instead of once per layer. The shareable set is derived from the checkpoint's own adapter tensors, so a tail with an adapter (ffn_up, whose MLP adapter is baked per layer) stays per layer, and a checkpoint that adds attention adapters needs no code change. - src/models/zamba2.cpp: the shared tensors are TENSOR_NOT_REQUIRED, and after the layer loop a hybrid layer that carries no copy aliases attn_post_norm/wq/wk/wv/wo/ffn_norm/ffn_down from sources[visit % num_blocks]. The sources must be the first hybrid layers of the file, which is asserted. A file with a copy in every layer -- every file written before this change -- makes every layer a source, so each alias is the identity and the old path is unchanged. 7B-Instruct: 848 -> 771 tensors, 10.977 B -> 8.433 B params (20.45 -> 15.71 GiB BF16). The remaining 11 gate_up copies need the adapter applied in the graph, which is cause 2 of ggml-org#1036 and stays open.
|
#203 ready for review at 9ad387f Cause 1 of engine ggml-org#1036, fork side. The block is stored once per block and the loader aliases the Two things a reviewer should push on: (a) the sources must be the first hybrid layers of the file |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 9ad387fb9 — the loader and the converter agree tail for tail and source layer for source layer, and both file generations resolve correctly.
The two halves match exactly. The loader aliases seven tensors (attn_post_norm, wq, wk, wv, wo, ffn_norm, ffn_down) and the converter's _SHARED_TAILS is the same seven (input_layernorm.weight, pre_ff_layernorm.weight, self_attn.q/k/v/o_proj.weight, feed_forward.down_proj.weight), with ffn_up excluded on both sides because the per-layer gate_up_proj_adapter_list is baked into it — the loader keeps ffn_up at flag 0 (required, per layer) while every shared tail becomes TENSOR_NOT_REQUIRED.
The modulo is sound, which is the thing that could have been silently wrong. The converter builds _shared_block_src = _hybrid_layers[:num_mem_blocks] (line 55) and maps visit % num_mem_blocks to a block (line 51); the loader asserts shared_src[k] == shared_use[k] — i.e. the copies are a prefix of the hybrid layers in visit order — and then aliases sources[v % sources.size()]. Those are the same mapping, and the assert is what makes the environment's % equal the converter's.
Both file generations land correctly. Old file: every hybrid layer carries a copy, so every layer is a source, |sources| == |hybrid| == |use|, and v % n == v makes every alias the identity — the old path is preserved exactly, as claimed. New file: sources are the first num_mem_blocks hybrid layers and the rest alias into them. A file with no copy at all is refused once by the two named asserts rather than once per tensor.
Compiled here (CPU Release, -DGGML_HRX=OFF): src/models/zamba2.cpp builds to zamba2.cpp.o with zero warnings and zero errors in the whole build, and python3 -m py_compile conversion/zamba2.py is clean.
Not verified by me: the end-to-end conversion. There is no Zamba2 checkpoint on this box, so the parameter/tensor-count claims (10.977 B → 7.357 B, 13 copies → 2, and every quant arm inheriting the inflation) are the author's. What I can say is that the code that produces them is consistent with the code that consumes them, which is where this class of change usually breaks.
Two residual notes, neither blocking:
- The loader's modulo trusts
sources.size() == num_mem_blocks, and the GGUF carries nonum_mem_blocks. A file produced by this converter always satisfies it; a hand-built or partially-shared file where the copies are a correct prefix but fewer than the true block count would alias wrong weights silently. The prefix assert catches the ordering error but not that one. Writingnum_mem_blocksinto the GGUF hparams would close it whenever the format is next touched. TENSOR_NOT_REQUIREDis per tensor while the "no copy at all" assert keys onwqonly. A file that carrieswqbut omits, say,ffn_downeverywhere would alias a null pointer into every layer that needs it instead of hitting the named assert. Again converter-impossible; cheap to widen the assert to the whole set.
|
Device arm added to the body (the section that said it was not measured). 2.7B, old layout vs new layout,
Exit 0 under STRICT means no node of the graph was placed on anything but HRX, so the aliased graph keeps the Two things I owe the reviewer as honest reporting rather than a clean story: the first device attempt burned a 1830 s |
…r the VL class is never built Registering `Zamba2VLTextModel` is not enough: `get_model_architecture()` prefers `text_config.architectures[0]` for the text pass, and Zamba2-VL's `text_config` says `Zamba2ForCausalLM`. A conversion therefore built the **plain** `Zamba2Model`, so the ported `set_vocab` never ran and the output carried Zyphra's `chat_template.jinja` (74475f26d1…, 796 B) and an unpadded 32004-token vocab instead of the published 1045-byte template and 32064 tokens — a silent wrong artifact, which is exactly what engine#1098 is about. `conversion/base.py`'s TEXT exception list already exists for this shape of model (`StepVLForConditionalGeneration`, `Sarashina2VisionForCausalLM`, `Exaone4_5_…`, `Step3p7_…`); it now names `Zamba2_VLForConditionalGeneration` too, with the reason in a comment. Measured on `Zyphra/Zamba2-VL-1.2B`, converted end to end through this branch and compared with the published `1bit-MONSTER/Zamba2-VL-1.2B-GGUF/Zamba2-VL-1.2B-Q8_0.gguf`: | | published | before this commit | after | |---|---|---|---| | `tokenizer.chat_template` | `9df322391a…` (1045 B) | `74475f26d1…` (796 B) | **`9df322391a…` (1045 B)** | | tokens | 32064 | 32004 | **32064** | | architecture | zamba2 | zamba2 | zamba2 | | tensors | 398 | 378 | 378 | The 20-tensor difference is the pin's own #203 ("store Zamba2's shared transformer block once per block"): the published files predate it and carry `blk.11.*` / `blk.17.*` a second time. It is not a port difference, and a re-quantise from this tree would not reproduce those 20 tensors.
… published GGUFs have an authoring site (engine#1098) (#206) * convert: Zamba2-VL (Zamba2 language model + Qwen2.5-VL tower), so the published GGUFs have an authoring site The three published `1bit-MONSTER/Zamba2-VL-*` GGUFs carry the template `9df322391abffe14587e6f3cfaea42420eaffec364835debfcb13c0e4c6eeab6` (1045 B), and the code that wrote it exists only in the Vulkan-era checkout (`llama.cpp-vulkan/conversion/zamba2vl.py`, now removed with that build, RFC #213) — so a pin bump could not re-derive those files (engine ggml-org#1098). This brings the module to the pinned tree, unchanged and registered: - `conversion/zamba2vl.py`: `Zamba2VLTextModel` (the `language_model.` half, `MODEL_ARCH.ZAMBA2`) and `Zamba2VLVisionModel` (the `vision_tower.` half as a Qwen2.5-VL `MmprojModel`), i.e. the model + mmproj pair the repos ship. `_CHAT_TEMPLATE` is Zyphra's own multimodal format — `<s>`-delimited turns, `<|vision_start|><image><|vision_end|>` — with the media markers as llama.cpp's `<__media_<id>__>` bridge, which is what makes the file usable by mtmd. - `conversion/__init__.py`: `"Zamba2_VLForConditionalGeneration": "zamba2vl"` in both `TEXT_MODEL_MAP` and `MMPROJ_MODEL_MAP`, so `load_all_models()` imports it like every other family. Verified against the pinned tree (no weights needed): both classes register for `Zamba2_VLForConditionalGeneration` (`ModelType.TEXT` -> `Zamba2VLTextModel`, arch `MODEL_ARCH.ZAMBA2` = 67; `ModelType.MMPROJ` -> `Zamba2VLVisionModel`, arch 1), and the module's `_CHAT_TEMPLATE` hashes to `9df322391abffe14…`, 1045 B — byte-identical to the template in all three published files. Pre-existing, untouched: `load_all_models()` at this pin already warns for `cambrian`, `deepseek41`, `flint`, `qwen4exp` and `dots3` (`Missing property 'model_arch'`, `no attribute 'example'`, a missing module); this change adds no warning. Engine side picks this up with the next llama.cpp pin bump; nothing in the engine changes here. * convert: keep Zamba2-VL's top-level architecture for the text pass, or the VL class is never built Registering `Zamba2VLTextModel` is not enough: `get_model_architecture()` prefers `text_config.architectures[0]` for the text pass, and Zamba2-VL's `text_config` says `Zamba2ForCausalLM`. A conversion therefore built the **plain** `Zamba2Model`, so the ported `set_vocab` never ran and the output carried Zyphra's `chat_template.jinja` (74475f26d1…, 796 B) and an unpadded 32004-token vocab instead of the published 1045-byte template and 32064 tokens — a silent wrong artifact, which is exactly what engine#1098 is about. `conversion/base.py`'s TEXT exception list already exists for this shape of model (`StepVLForConditionalGeneration`, `Sarashina2VisionForCausalLM`, `Exaone4_5_…`, `Step3p7_…`); it now names `Zamba2_VLForConditionalGeneration` too, with the reason in a comment. Measured on `Zyphra/Zamba2-VL-1.2B`, converted end to end through this branch and compared with the published `1bit-MONSTER/Zamba2-VL-1.2B-GGUF/Zamba2-VL-1.2B-Q8_0.gguf`: | | published | before this commit | after | |---|---|---|---| | `tokenizer.chat_template` | `9df322391a…` (1045 B) | `74475f26d1…` (796 B) | **`9df322391a…` (1045 B)** | | tokens | 32064 | 32004 | **32064** | | architecture | zamba2 | zamba2 | zamba2 | | tensors | 398 | 378 | 378 | The 20-tensor difference is the pin's own #203 ("store Zamba2's shared transformer block once per block"): the published files predate it and carry `blk.11.*` / `blk.17.*` a second time. It is not a port difference, and a re-quantise from this tree would not reproduce those 20 tensors. --------- Co-authored-by: agent <agent@local>
… it (engine ggml-org#1036 cause 2) The remaining cause of engine ggml-org#1036. #203 stopped storing the shared transformer block once per hybrid layer; this stops storing the per-layer MLP adapter baked into a full FFN matrix, which was the last 1.13 B params / 2.26 GB of the 7B. The duplication was never in the checkpoint: its `gate_up_proj.weight` appears exactly twice (one per shared block) and the per-layer differences live in `gate_up_proj_adapter_list.<visit>.<0|1>`. Our converter merged each layer's rank-128 pair into its own full [n_embd, 2*n_ff] copy. HF applies it as `gate_up_proj(x) + adapter(x)` with no LoRA scaling (modeling_zamba2.py:960-962), on the same normed FFN input and before the gate/up split. So: `gate_up_proj` joins the shared tails (written once per block, aliased by the loader through the mechanism #203 added), and each `gate_up_proj_adapter_list.<visit>.<0|1>` is written as `blk.<layer>.ffn_gate_up_adapter_a/_b.weight`. A new key `zamba2.ffn_gate_up_adapter_rank` both discriminates the two forms (absent/0 = the pre-adapter form) and sizes the tensors. In the graph the pair is applied as base + B*(A*x) through one defaulted parameter on build_ffn, so no other arch's call site changes; the loader, the saver and the arch test all handle both forms, and old GGUFs keep working as a code path rather than a compatibility shim. 2.7B, CPU, one binary for both forms: BF16 6,009,822,848 -> 5,328,903,360 B (-681 MB), tensors 520 -> 531 with ffn_up 9 -> 2 and 9+9 adapters, PPL 16.7937 -> 16.795417 (0.0102 %), KLD 0.000067 +- 0.000002 against the merged file's own logits with same top-p 99.510 %, calibration floor 0.000000, greedy identical. The residual is structural: the merged file has up @ down computed in f32 at conversion time and rounded to bf16, the adapter path applies the pair at run time. Quantized (what gets published): Q5_K_M merged 1994.93 MiB / PPL 17.0430, adapter 1773.11 MiB / PPL 16.9775 - 221.8 MiB smaller and 0.4 % better on the same window. test-llama-archs: zamba2 OK (0.00e+00) + roundtrip OK, with the synthetic model declaring the adapter form so the new tensors are exercised by the saver round-trip. Quantized files need one more thing, found by reading the HRX predicate before the device could say it: the adapter's B contraction has input_size = rank = 128, which the float weight path accepts (input_size >= 4 && % 4 == 0, #208) but the block formats do not (their block layout needs >= 256). So a default Q5_K_M quantized the pair, dropped that matmul on all 9 hybrid layers to the CPU - measured on gfx1151 as 9 of 3051 nodes off HRX and prefill 463.66 vs 589.47 tok/s (0.79x) - while every BF16 arm said "fully on HRX". llama-quant now keeps the pair at F16 by default for 31.8 MiB of a 1804.93 MiB file (explicit --tensor-type still wins, --pure is unchanged), and the resulting file is byte-identical to the one measured strict-clean on the device. Device arms (gfx1151, gpu-hold), BF16 both forms, same binary: GGML_HRX_STRICT=1 exit 0 on all four (prefill and decode, merged and adapter), pp512 441.98 -> 440.57 and tg64 22.87 -> 22.60 (0.997 / 0.988, i.e. no measurable cost), device PPL 19.3635 both.
… it (engine ggml-org#1036 cause 2) The remaining cause of engine ggml-org#1036. #203 stopped storing the shared transformer block once per hybrid layer; this stops storing the per-layer MLP adapter baked into a full FFN matrix, which was the last 1.13 B params / 2.26 GB of the 7B. The duplication was never in the checkpoint: its `gate_up_proj.weight` appears exactly twice (one per shared block) and the per-layer differences live in `gate_up_proj_adapter_list.<visit>.<0|1>`. Our converter merged each layer's rank-128 pair into its own full [n_embd, 2*n_ff] copy. HF applies it as `gate_up_proj(x) + adapter(x)` with no LoRA scaling (modeling_zamba2.py:960-962), on the same normed FFN input and before the gate/up split. So: `gate_up_proj` joins the shared tails (written once per block, aliased by the loader through the mechanism #203 added), and each `gate_up_proj_adapter_list.<visit>.<0|1>` is written as `blk.<layer>.ffn_gate_up_adapter_a/_b.weight`. A new key `zamba2.ffn_gate_up_adapter_rank` both discriminates the two forms (absent/0 = the pre-adapter form) and sizes the tensors. In the graph the pair is applied as base + B*(A*x) through one defaulted parameter on build_ffn, so no other arch's call site changes; the loader, the saver and the arch test all handle both forms, and old GGUFs keep working as a code path rather than a compatibility shim. 2.7B, CPU, one binary for both forms: BF16 6,009,822,848 -> 5,328,903,360 B (-681 MB), tensors 520 -> 531 with ffn_up 9 -> 2 and 9+9 adapters, PPL 16.7937 -> 16.795417 (0.0102 %), KLD 0.000067 +- 0.000002 against the merged file's own logits with same top-p 99.510 %, calibration floor 0.000000, greedy identical. The residual is structural: the merged file has up @ down computed in f32 at conversion time and rounded to bf16, the adapter path applies the pair at run time. Quantized (what gets published): Q5_K_M merged 1994.93 MiB / PPL 17.0430, adapter 1773.11 MiB / PPL 16.9775 - 221.8 MiB smaller and 0.4 % better on the same window. test-llama-archs: zamba2 OK (0.00e+00) + roundtrip OK, with the synthetic model declaring the adapter form so the new tensors are exercised by the saver round-trip. Quantized files need one more thing, found by reading the HRX predicate before the device could say it: the adapter's B contraction has input_size = rank = 128, which the float weight path accepts (input_size >= 4 && % 4 == 0, #208) but the block formats do not (their block layout needs >= 256). So a default Q5_K_M quantized the pair, dropped that matmul on all 9 hybrid layers to the CPU - measured on gfx1151 as 9 of 3051 nodes off HRX and prefill 463.66 vs 589.47 tok/s (0.79x) - while every BF16 arm said "fully on HRX". llama-quant now keeps the pair at F16 by default for 31.8 MiB of a 1804.93 MiB file (explicit --tensor-type still wins, --pure is unchanged), and the resulting file is byte-identical to the one measured strict-clean on the device. Device arms (gfx1151, gpu-hold), BF16 both forms, same binary: GGML_HRX_STRICT=1 exit 0 on all four (prefill and decode, merged and adapter), pp512 441.98 -> 440.57 and tg64 22.87 -> 22.60 (0.997 / 0.988, i.e. no measurable cost), device PPL 19.3635 both.
… it (engine ggml-org#1036 cause 2) (#218) The remaining cause of engine ggml-org#1036. #203 stopped storing the shared transformer block once per hybrid layer; this stops storing the per-layer MLP adapter baked into a full FFN matrix, which was the last 1.13 B params / 2.26 GB of the 7B. The duplication was never in the checkpoint: its `gate_up_proj.weight` appears exactly twice (one per shared block) and the per-layer differences live in `gate_up_proj_adapter_list.<visit>.<0|1>`. Our converter merged each layer's rank-128 pair into its own full [n_embd, 2*n_ff] copy. HF applies it as `gate_up_proj(x) + adapter(x)` with no LoRA scaling (modeling_zamba2.py:960-962), on the same normed FFN input and before the gate/up split. So: `gate_up_proj` joins the shared tails (written once per block, aliased by the loader through the mechanism #203 added), and each `gate_up_proj_adapter_list.<visit>.<0|1>` is written as `blk.<layer>.ffn_gate_up_adapter_a/_b.weight`. A new key `zamba2.ffn_gate_up_adapter_rank` both discriminates the two forms (absent/0 = the pre-adapter form) and sizes the tensors. In the graph the pair is applied as base + B*(A*x) through one defaulted parameter on build_ffn, so no other arch's call site changes; the loader, the saver and the arch test all handle both forms, and old GGUFs keep working as a code path rather than a compatibility shim. 2.7B, CPU, one binary for both forms: BF16 6,009,822,848 -> 5,328,903,360 B (-681 MB), tensors 520 -> 531 with ffn_up 9 -> 2 and 9+9 adapters, PPL 16.7937 -> 16.795417 (0.0102 %), KLD 0.000067 +- 0.000002 against the merged file's own logits with same top-p 99.510 %, calibration floor 0.000000, greedy identical. The residual is structural: the merged file has up @ down computed in f32 at conversion time and rounded to bf16, the adapter path applies the pair at run time. Quantized (what gets published): Q5_K_M merged 1994.93 MiB / PPL 17.0430, adapter 1773.11 MiB / PPL 16.9775 - 221.8 MiB smaller and 0.4 % better on the same window. test-llama-archs: zamba2 OK (0.00e+00) + roundtrip OK, with the synthetic model declaring the adapter form so the new tensors are exercised by the saver round-trip. Quantized files need one more thing, found by reading the HRX predicate before the device could say it: the adapter's B contraction has input_size = rank = 128, which the float weight path accepts (input_size >= 4 && % 4 == 0, #208) but the block formats do not (their block layout needs >= 256). So a default Q5_K_M quantized the pair, dropped that matmul on all 9 hybrid layers to the CPU - measured on gfx1151 as 9 of 3051 nodes off HRX and prefill 463.66 vs 589.47 tok/s (0.79x) - while every BF16 arm said "fully on HRX". llama-quant now keeps the pair at F16 by default for 31.8 MiB of a 1804.93 MiB file (explicit --tensor-type still wins, --pure is unchanged), and the resulting file is byte-identical to the one measured strict-clean on the device. Device arms (gfx1151, gpu-hold), BF16 both forms, same binary: GGML_HRX_STRICT=1 exit 0 on all four (prefill and decode, merged and adapter), pp512 441.98 -> 440.57 and tg64 22.87 -> 22.60 (0.997 / 0.988, i.e. no measurable cost), device PPL 19.3635 both. Co-authored-by: agent <agent@local>
convert/model: store Zamba2's shared transformer block once per block (engine ggml-org#1036)
Every hybrid layer of a Zamba2 model runs the same transformer block, and the checkpoint stores
that block only
num_mem_blockstimes (twice at 7B:model.layers.{6,11}.shared_transformer.*).Our converter wrote one copy per hybrid layer instead — 13 copies of two blocks — so
ref/Zamba2-7B-Instruct-bf16.ggufis 10.977 B parameters / 20.45 GiB for a 7.357 B / 14.8 GBcheckpoint, and every quant arm inherits the inflation.
This writes each block once and makes the loader point the other layers at it.
The change
conversion/zamba2.py— the sharing rule was already computed (visit % num_mem_blocks, sources= the first
num_mem_blockshybrid layers); the code then duplicated at the end of it. A blocktail is now written once, at the block's source layer. Which tails are shareable is derived from
the checkpoint's own adapter tensors, not hardcoded: a tail that has a per-layer adapter is
per-layer after merging and stays per layer.
ffn_upcarries the per-layer MLP adapter(
feed_forward.gate_up_proj_adapter_list.<visit>, 13 of them at 7B), so it is not shared.src/models/zamba2.cpp— the shared set is createdTENSOR_NOT_REQUIRED. After the layer loop,the hybrid layers that actually carry the block are the sources, and every other hybrid layer
aliases
attn_post_norm / wq / wk / wv / wo / ffn_norm / ffn_downfromsources[visit % num_blocks]. No new GGUF key, no graph change, no new op.Old files keep working with the same code, on the old path. A file written before this change
has a copy in every hybrid layer, so every layer is a source,
|sources| == |hybrid|, and eachalias is to itself. A file that stores no copy at all is refused once with a named assertion
instead of failing per tensor, and the sources must be the first hybrid layers of the file —
that is what makes the modulo sound, and it is asserted rather than assumed.
Measured
Block multiplicity in the current
ref/Zamba2-7B-Instruct-bf16.gguf(13 copies of each tensor,head/mid/tail sha256 per tensor):
attn_q,attn_k,attn_v,attn_outputpost_attention_norm,ffn_norm,ffn_downffn_up(baked MLP adapter)ssm_mixand the 2 distinct values follow
visit % num_mem_blocksper layer ([0,1,0,1,...]over the13 hybrid layers), which is exactly the rule the loader now uses.
Converter, run over the real 7B-Instruct checkpoint's shapes (
~/work/zarch/proof/1036-conv-check.py,reads safetensors headers only):
= 2,543,439,360 params reclaimed: one block minus
gate_up(q+k+v 51,380,224 each +attn_output25,690,112 +ffn_down51,380,224 + the two norms) = 231,221,760, times the 11duplicate layers.
ffn_upandssm_mixare still written once per hybrid layer.2.7B fixture, new layout built from the old GGUF (
~/work/zarch/proof/1036-prune.py, which derivesthe block assignment from the file and refuses one that is not
visit % k): 569 -> 520 tensors,3.828 B -> 3.003 B params, 7.14 -> 5.60 GiB, the 14 source-layer tensors byte-identical to the
originals and all 49 duplicates gone.
Equivalence, same binary, CPU (
GGML_HRX=OFF), 4 threads:The capital of France is Paris.The capital of France is Paris.and B against A's own saved logits,
llama-perplexity --kl-divergenceover the same 16 chunks(8192 tokens): Mean KLD 0.000000 +/- 0.000000,
Same top p100.000 +/- 0.000 %,Δp RMS 0.001 %. Not "close" — identical to the last digit, which is what dropping genuinely
duplicated bytes has to give.
Arm A reproducing 16.7937 also re-proves the identity path: the published 2.7B BF16 number was
taken before this change.
Same two files on the device (gfx1151,
--device HRX0 -ngl 99, undergpu-hold):GGML_HRX_STRICT=1aborts on the first node that is not placed on HRX, so exit 0 is the strong form of "no CPUfallback"; both layouts give it, and the device PPL agrees to every printed digit. New layout throughput,
-r 1on adie shared with other lanes: pp512 363.23 tok/s, tg64 15.15 tok/s — indicative only.
test-llama-archs -a zamba2: NMSE0.00e+00vs CPU, roundtripOK.Not reclaimed here (cause 2 stays open)
The remaining 1,130,364,928 params — 11 copies of
gate_up(102,760,448 each, 2.26 GiB) — cannotbe dropped without changing the graph: the adapter has to be applied at run time instead of being
baked. That is a separate decision, so engine ggml-org#1036 stays open for it; this PR closes cause 1 only.
Limits of what was run
new-layout file is built by pruning the old GGUF rather than by re-running the converter. The
converter's side is checked against the real 7B-Instruct checkpoint (emitted tensor set and
parameter total above) but the 7B file itself is pi-zquant's to rebuild, sequenced after their
current sweep reads
ref/.never changes an op, so identical placement is the expected result. It was measured once,
-r 1, on a die thatwas busy with other lanes for the ~45 minutes the queue needed — the tok/s above is therefore indicative and
should not be compared with anything. Placement and device PPL are equalities, and those are comparable.
numbers that matter are equalities, not tok/s.