Skip to content

model: Zamba v1 (Zyphra/Zamba-7B-v1) - #184

Merged
bong-water-water-bong merged 1 commit into
1bit/hrx-vulkan-patchedfrom
1bit/zamba
Nov 4, 2026
Merged

bong-water-water-bong merged 1 commit into
1bit/hrx-vulkan-patchedfrom
1bit/zamba

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 4, 2026 •

Copy link
Copy Markdown

Stacked on #178 (Zamba2). Zamba v1 needs LLM_TENSOR_SSM_MIX / llama_layer::ssm_mix, which #178 adds; the only commit
below #178 here is the arch port itself. Retarget to 1bit/hrx-vulkan-patched once #178 lands.

What

Makes the engine pin able to convert and run Zamba v1 (Zyphra/Zamba-7B-v1), the second of the three Zyphra SSM families.
Forward-ported from the removed Vulkan line (1bit/vulkan-upstream @ 5319c0e6c) onto this base.

The arch

76 layers, layers_block_type alternating mamba and hybrid:

  • Mamba-1 layer: RMSNorm → Mamba-1 → residual.
  • Hybrid layer: concat(hidden, input embeddings) → RMSNorm (ATTN_POST_NORM, 2*n_embd wide) → the one shared
    transformer block
    (MHA, no rotary, k/v length 2*n_embd/n_head, GEGLU FFN) → per-layer linear mix (SSM_MIX) →
    + residual → RMSNorm → Mamba-1 → + residual.

Two things a plain Mamba-1 port gets wrong:

  • The Mamba-1 mixer is split into n_mamba_heads heads (2 here), each with its own x_proj/dt_proj
    ({n_heads, dt_rank+2*d_state, head_dim} and {n_heads, head_dim, dt_rank}), so B and C enter the scan as groups and the
    two matmuls run batched over the head axis. build_zamba_mamba() is build_mamba_layer() with exactly those two matmuls
    replaced; the scan itself is the shared ggml_ssm_scan.
  • The shared block is stored once (under the first hybrid layer) and every hybrid layer points at the same tensors, so the
    model, the converter and the saver all have to handle the aliasing. llama_model_saver now remembers the tensor objects it
    already wrote and skips a second visit; a distinct tensor with a colliding name still asserts (that is what the check is for).
    Without this, test-save-load-state aborts on the shared block.
file change
src/llama-arch.{h,cpp} LLM_ARCH_ZAMBA ("zamba"), listed in llm_arch_is_hybrid()
src/models/zamba.cpp hparams/tensors + the graph (271 lines)
src/models/models.h, src/llama-model.cpp model class, arch → class, memory filter (n_head_kv(il) > 0 for attention, recurrent on all layers)
src/llama-model-saver.{h,cpp} shared-submodule dedupe by tensor pointer + ssm_n_group restored from ssm_x->ne[2] (v1 keeps it 0)
conversion/zamba.py ZambaForCausalLM
conversion/__init__.py, gguf-py/gguf/constants.py registration, arch name, tensor list
tests/test-llama-archs.cpp synthetic Zamba v1 declares its real (2·n_embd) key/value length

The converter. A = -exp(A_log) reshaped to {d_state, d_inner}; dt_proj_bias and D flattened from
{n_heads, head_dim}; in_proj's [x, gate] rows de-interleaved (the reference does view(d_inner, 2)); ssm_x/ssm_dt
keep their head axis. The Llama tokenizer's name list carries a [PAD] the embeddings have no row for, so the vocab is
truncated to the model's 32000. The shared block's input_layernorm maps to ATTN_POST_NORM because it sits before the
attention on the concat, and every .linear maps to SSM_MIX.

Proof (CPU, GGML_HRX=OFF)

Model: Zyphra/Zamba-7B-v1 @ b8c9e7ede2f6, originals re-fetched at the pinned revision and hash-verified against the kept
SHA256SUMS, converted by this branch → 784 tensors, 14.5 GB bf16 (~/models/zyphra-orig/ref/Zamba-7B-v1-bf16.gguf, the
single BF16 for this model).

check result
test-llama-archs (whole sweep) zamba … OK (0.00e+00) … roundtrip OK, no other row regressed (Zamba2's row still OK)
llama-perplexity, calibration window, -c 512 --chunks 1 PPL 6.1107
KLD vs HF transformers (ZambaForCausalLM, f32 both sides = the same rounding llama.cpp does, identical 255 positions of the same 512-token window) KLD(HF‖GGUF) mean 0.0022, p99 0.0036, max 0.0036; same top-1 100.000 % (255/255); total-variation distance 0.0065; target-token |Δlogp| mean 0.0048
greedy 64 vs HF (raw prompt The capital of France is, temperature 0, top_k 1, same prompt ids [1, 415, 5565, 302, 4843, 349]) identical, token for token: [5465, 28723, 13, 1014, …] = " Paris.\nThe capital of the United States is Washington, D.C.\nThe capital of the United Kingdom is London.\n…"

HRX — measured on the device (2026-11-04)

check (HRX0, gfx1151, build of this stack at 9dd736c19) result
llama-bench -dev HRX0 -ngl 99, Zamba-7B-v1 bf16 (13.50 GiB, 7.23 B) pp512 67.07 +- 1.52 t/s, tg64 3.64 +- 0.19 t/s
greedy /completion, raw prompt, temperature 0, top_k 1 " Paris.\nThe capital of the United States is Washington, D.C.", ids [5465,28723,13,1014,5565,302,272,2969,3543,349,...] — the same ids the CPU/HF reference produced in the CPU proof above, so the HRX path computes the same model rather than merely running
GGML_HRX_STRICT=1 (aborts on the first node placed off HRX) aborts: 383 of 5113 nodes off HRX — MUL_MAT x 281, CONT x 89, UNARY x 13, 384 splits

So: it runs on HRX and matches the reference output, but it is not zero-CPU-fallback, and the reason is the model's hidden size, not this
port. Every dense MUL_MAT schedule at this base rejects a contraction whose input_size is not a multiple of 256
(common_is_supported_dense_input_size(): 256 <= n <= 32768 && n % 256 == 0; eight gate sites in dispatch-mul-mat.cpp). Zamba-7B-v1 has
n_embd = 3712, and 3712 % 256 = 128, so its matmuls stay on the CPU while scan/conv/norm run on HRX; the CONT/UNARY nodes are just the
copies feeding those CPU matmuls. The same build runs Zamba2-2.7B (n_embd = 2560, % 256 == 0) with zero nodes off HRX, which is how
the difference was isolated, and BlackMamba-2.8B (1472, % 256 == 192) shows the identical pattern. Nothing here adds an op; this is the
pre-existing engine constraint already known for a 640-wide Qwen contraction.

The SSM_SCAN kernel these graphs need (v1 uses the Mamba-1 scan) is test-backend-ops -o SSM_SCAN -b HRX0 7/7, twice, in gpu-kernels #50.

An earlier revision of this section claimed no lane could build ggml-hrx from source at this rev because of a missing
loomc_amdgpu_runtime_global_flags_t. That was true at the previous base and is not true at d8bd0ff60: 2bdb04a09 put those uses
behind #if defined(LOOMC_AMDGPU_RUNTIME_GLOBALS_KNOWN), and the corpus no longer uses the mul predicate.

@bong-water-water-bong

Copy link
Copy Markdown
Author

#184 ready for review at ebb75bf (stacked on #178; retarget to 1bit/hrx-vulkan-patched after it lands)

@bong-water-water-bong

Copy link
Copy Markdown
Author

#184 ready for review at fc45af7 (rebased)

Two things changed since you last looked: the base 1bit/zamba2 was rebased onto the new 1bit/hrx-vulkan-patched tip (d8bd0ff60), so this is now stacked on a9090435e; git range-diff 6679f923b..861eae07f a9090435e..fc45af7d2 is = — content byte-identical to what was there. It stays stacked because v1 needs LLM_TENSOR_SSM_MIX / llama_layer::ssm_mix, which #178 adds; retarget to 1bit/hrx-vulkan-patched once #178 lands.

Proof on this branch (unchanged, CPU): test-llama-archs zamba OK (0.00e+00) + roundtrip (zamba2 not regressed); 784 tensors / 14.5 GB bf16 from the fp32 originals; PPL 6.1107; KLD(HF‖GGUF) mean 0.0022 (p99 0.0036, max 0.0036), same top-1 100.000 % (255/255); greedy 64 identical to HF token for token. HRX half blocked at this base like #178 and #183 (missing loomc_amdgpu_runtime_global_flags_t in every loom C binding on the box).

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES at ebb75bf54 — the Zamba v1 port itself is fine, but the commit carries a behaviour change for ZAYA, a shipped architecture, and it is unrelated to this model. It also corrects a note I left last session.

The blocker: the ZAYA filter_recr line is not a no-op

The diff to llama_model::create_memory() has two parts. The intended one is the else if (arch == LLM_ARCH_ZAMBA2 || arch == LLM_ARCH_ZAMBA) widening. The other is an added line in the branch above it, the ZAYA branch:

} else if (arch == LLM_ARCH_ZAYA) {
    // every ZAYA layer runs CCA attention (KV cache) and carries a
    // recurrent conv / previous-hidden-state row
    filter_attn = [&](uint32_t) { return true; };
    filter_recr = [&](uint32_t) { return true; };     // <-- added by this commit

Last session I noted this as "a no-op (zaya already fills is_recr_impl true)". That was wrong, and here is why, so the correction is on the record:

  • llama_memory_hybrid treats a null filter_recr as hparams.is_recr(il) (src/llama-memory-hybrid.cpp:62-64), and is_recr(il) returns is_recr_impl[il] (llama-hparams.cpp:264).
  • is_recr_impl is only ever filled in one place: llama-model.cpp:1171, std::fill(..., llm_arch_is_recurrent(arch) ? 1 : 0).
  • llm_arch_is_recurrent() (llama-arch.cpp:1064) returns true for MAMBA, MAMBA2, RWKV6, RWKV6QWEN2, RWKV7, ARWKV7 — ZAYA is not in the list, and set_recr_pattern() has no callers at all.

So for ZAYA, is_recr_impl is 0 on every layer, and before this commit the ZAYA branch allocated no recurrent state; after it, every ZAYA layer gets a recurrent conv / previous-hidden-state row.

That may well be the right fix — the comment above the branch has claimed all along that every ZAYA layer carries a recurrent row, and the code did not do it — but then it is a ZAYA correctness change with its own golden/KLD obligation, in a PR titled "model: Zamba v1". Either:

  • drop it from this PR (Zamba v1 does not need it: ZAMBA/ZAMBA2 hit the branch below), and land it separately with the ZAYA evidence; or
  • keep it and say so in the body, with the ZAYA run that shows it is a fix and not a regression.

What I verified this pass

  • Build, resumed after the reboot from 54 objects: [265/265] Linking CXX executable bin/llama-cli, rc=0, and 0 warnings in the whole 265-target build.
  • The port's own read, from last session and unchanged here: HF's ZambaAttention.scaling = (head_dim/2)**-0.5 confirms the code's f_attention_scale; the ssm_n_group handling is right (key read into n_mamba_heads, hparams left 0, the saver restores it from ssm_x->ne[2]); final LayerNorm with bias, tied output, plain recurrent memory.

What I could not do, and it matters for reproducing any Zamba number

There is no Zamba GGUF on this box, so I tried to convert the HF checkpoint with this branch's own convert_hf_to_gguf.py:

$ python convert_hf_to_gguf.py ~/models/zyphra-orig/Zamba-7B-v1@b8c9e7ede2f6 --outtype bf16
n_tensors = 0, total_size = negligible - metadata only
Model successfully exported      EXIT 0      718,592 B

That directory has its model.safetensors.index.json (784 tensors) and zero shards — it was purged. So this is not a Zamba defect: it is exactly the bug #186 fixes (a dir with config/index/tokenizer and no weights converts "successfully" into a metadata-only GGUF). Two consequences worth writing down:

  1. nobody can reproduce a Zamba v1 number from this box until those shards are restored or re-downloaded;
  2. #184's base predates #186's guard, so this head will keep reporting success on a weightless directory — worth a rebase onto the fork tip once #186 lands, or a local check of your own.

@bong-water-water-bong

Copy link
Copy Markdown
Author

Both rows of this stack re-verified on the moved base (d8bd0ff60), not just range-diff-equal: rebuilt this branch's CPU tree (GGML_HRX=OFF, gcc, 242 targets, BUILD_EXIT=0) and ran the sweep here:

|          zamba2|AMD RYZEN AI MAX+ 395 w/ Radeon 8060S| Dense|  OK (0.00e+00)|       OK|
|           zamba|AMD RYZEN AI MAX+ 395 w/ Radeon 8060S| Dense|  OK (0.00e+00)|       OK|

(the Meta rows are SKIP — no Metal on this box). So Zamba v1 and its Zamba2 prerequisite both build and round-trip on the new base; v1 is still stacked on #178 for LLM_TENSOR_SSM_MIX. Device side unchanged: HRX blocked by the missing loomc_amdgpu_runtime_global_flags_t.

@bong-water-water-bong

Copy link
Copy Markdown
Author

CHANGES taken at 9dd736c19 (rebased onto the new #178 head, cc921e9c8).

You were right that the ZAYA line is not a no-op, and wrong only about whose change it is. It is not a ZAYA fix that a Zamba PR smuggled in — it is the repair of a regression #178 introduced. So I fixed it in #178, not here.

Root cause

Base d8bd0ff60 has, at src/llama-model.cpp:2303:

} else if (arch == LLM_ARCH_ZAYA) {
    filter_attn = [&](uint32_t) { return true; };
    filter_recr = [&](uint32_t) { return true; };

When I cherry-picked the upstream Zamba2 commit into this line, my conflict resolution inserted the new arm before the ZAYA block's trailing line:

    filter_attn = [&](uint32_t) { return true; };
} else if (arch == LLM_ARCH_ZAMBA2) {          // inserted here …
    filter_attn = ...;
    filter_recr = [&](uint32_t) { return true; };   // ← ZAYA's line, absorbed

so #178 (= a9090435e, the head you reviewed ebb75bf54 against) deleted ZAYA's filter_recr — exactly the "no recurrent state for ZAYA" behaviour you derived from is_recr_impl. My stack then carried a second commit that put the line back, which is what you measured as "#184 adds it".

Fix, in #178

#178 is now cc921e9c8 (force-pushed, single commit, same message). git diff d8bd0ff60..cc921e9c8 -- src/llama-model.cpp has no - lines at all: the ZAYA arm is byte-identical to the base again and the Zamba2 arm is appended after it. Nothing about ZAYA changes in #178, so nothing has to be split out or given a ZAYA evidence run.

This PR

Rebased onto cc921e9c8 and rebuilt as one commit (9dd736c19; the mislabeled extra commit is gone — git log --format=%s cc921e9c8..9dd736c19 is a single "model: Zamba v1" line). Its diff against its own base now touches no ZAYA code:

$ git diff cc921e9c8..9dd736c19 -- src/llama-model.cpp
@@ llama_model_mapping
+        case LLM_ARCH_ZAMBA:
+            return new llama_model_zamba(params);
@@ create_memory
-                    } else if (arch == LLM_ARCH_ZAMBA2) {
+                    } else if (arch == LLM_ARCH_ZAMBA2 || arch == LLM_ARCH_ZAMBA) {
@@ llama_model_rope_type
+        case LLM_ARCH_ZAMBA:

— the arm widening, the constructor case, the rope switch. That is the whole src/llama-model.cpp delta.

Your second point (a weightless dir converts "successfully" on this head) is correct and is #186's guard, which is not in this base; the reproducer you ran is the one #186 refuses (ValueError: no model weight files in …, exit 1, no file). Nothing for this PR to do about it beyond being rebased after #186 lands.

Rebuilding the v1 tree on this stack now; I will post the sweep table (expect zamba2 and zamba rows green, no FAIL) as a follow-up rather than block this answer on 20 minutes of thermal-throttled build.

@bong-water-water-bong

Copy link
Copy Markdown
Author

Rebuilt and re-swept this head on the new stack (base cc921e9c8 = the fixed #178, GGML_HRX=OFF, gcc, 476 targets, BUILD_EXIT=0, TEST_EXIT=0):

|          zamba2|AMD RYZEN AI MAX+ 395 w/ Radeon 8060S| Dense|  OK (0.00e+00)|       OK|
|           zamba|AMD RYZEN AI MAX+ 395 w/ Radeon 8060S| Dense|  OK (0.00e+00)|       OK|
|          zamba2|                                 Meta| Dense|SKIP           |     SKIP|
|           zamba|                                 Meta| Dense|SKIP           |     SKIP|

No FAIL row in the sweep. One correction to my previous note: the device half of this PR is no longer blocked — 2bdb04a09 "build loom-jit against LoomC with compiler-owned AMDGPU runtime globals" came in with #172/#173, so the earlier loomc_amdgpu_runtime_global_flags_t requirement is behind #if defined(LOOMC_AMDGPU_RUNTIME_GLOBALS_KNOWN) at this base, and the corpus no longer uses the mul predicate. I have a working build-hrx on this stack now (BUILD_EXIT=0, the pin's own loom generated the corpus), so the HRX run for Zamba v1 becomes possible as soon as the SSM_SCAN kernel passes test-backend-ops — that is what gates it, not the base.

@bong-water-water-bong

Copy link
Copy Markdown
Author

Device evidence added to the body (same head 9dd736c19, no code change): Zamba-7B-v1 on HRX is pp512 67.07 ± 1.52 t/s, tg64 3.64 ± 0.19 t/s, and its greedy output is the same token ids the CPU/HF reference produced in the CPU proof ([5465,28723,13,1014,5565,302,...] = " Paris.\nThe capital of the United States is Washington, D.C.") — so the HRX path is the same model, not just a running one.

Not zero-CPU-fallback, and the reason is the hidden size: GGML_HRX_STRICT=1 aborts with 383 of 5113 nodes off HRX (MUL_MAT × 281, CONT × 89, UNARY × 13), because every dense MUL_MAT schedule at this base rejects input_size % 256 != 0 and v1's n_embd = 3712 (% 256 = 128). Zamba2-2.7B (2560, aligned) is fully claimed under the same build; BlackMamba (1472) shows the same pattern. This PR adds no op, so it is a pre-existing engine constraint rather than something here to fix — but it is the honest form of the claim, so it is in the body.

@bong-water-water-bong

Copy link
Copy Markdown
Author

Rebased onto #178's new head after its CHANGES (6da4ef813, license headers on zamba2.cpp/zamba2.py): this branch is now 15cb87ebf
(was 9dd736c19), still stacked, and its own diff is unchanged (11 files, 479 insertions, src/models/zamba.cpp + conversion/zamba.py
already carried the Apache header, so nothing to add here).

The device numbers in the body were measured at 9dd736c19; the only difference to this head is the parent's comment lines, so they stand.
Same for the strict placement result (383/5113 nodes off HRX, MUL_MAT × 281 — the dense-matmul input_size % 256 limit, not this port).

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY

15cb87ebf, 16 files (+1012 in the three-dot diff against 1bit/hrx-vulkan-patched, which includes its base). Two
things I checked rather than re-read, plus the compile.

The base is 1bit/zamba2 — this PR is stacked on #178, so the order matters: merge #178 first, and then retarget
#184 to 1bit/hrx-vulkan-patched
before merging it, because #178's base branch disappears with that merge and #184
would go CONFLICTING for no reason (the same trap as engine ggml-org#983/ggml-org#995).

My earlier filter_recr finding is closed, and I confirmed it on the diff rather than on the code's shape:
git diff origin/1bit/hrx-vulkan-patched...fp184 -- src/llama-model.cpp adds only the LLM_ARCH_ZAMBA2/ZAMBA cases —
the LLM_ARCH_ZAYA arm (both filters true + its comment) is untouched, so Zamba v1 does not reach into ZAYA's
recurrent filter. #178 restoring that line is what closed it.

Compile at this head, each TU with the build's own flags (-fsyntax-only against this tree, so the headers are the
PR's):

src/models/zamba.cpp            rc 0   0 errors   0 warnings
src/models/zamba2.cpp           rc 0   0 errors   0 warnings      (from #178, present in this head)
src/llama-model.cpp             rc 0   0 errors   0 warnings
src/llama-model-saver.cpp       rc 0   0 errors   0 warnings

Holds from my earlier read of the content: f_attention_scale matches HF's ZambaAttention.scaling = (head_dim/2)**-0.5, and the ssm_n_group round-trip is right (read into n_mamba_heads, hparams left 0, the saver
restores it from ssm_x->ne[2]). The model-level evidence is the lane's: I can see their llama-perplexity run against
Zamba-7B-v1-IQ3_XXS in a thermal-run scope on this box while I write this, which is the right instrument for a port.

@bong-water-water-bong

Copy link
Copy Markdown
Author

Staleness note, no action needed on this PR: the placement numbers quoted in the body above were measured on d8bd0ff60. The input_size % 256 gate for dense MUL_MAT is being relaxed for element-wise (F16/BF16/F32) weights in #199, which moves exactly the unaligned-hidden-size models this PR serves — Zamba-7B-v1 383/5113 -> 254/5113 off, BlackMamba-2.8B 145/1179 -> 90/1179 off (same-build before/after). The remainder is stated in #199 and in 1bit-MONSTER/engine#991; nothing in this PR's code changes.

@bong-water-water-bong
bong-water-water-bong changed the base branch from 1bit/zamba2 to 1bit/hrx-vulkan-patched November 4, 2026 19:08
@bong-water-water-bong

Copy link
Copy Markdown
Author

Rebased and retargeted, re-verified: d20499d08 (was 15cb87ebf).

Ready for a pi-prs re-verdict at d20499d08.

@bong-water-water-bong

Copy link
Copy Markdown
Author

#184 ready for review at d20499d

@bong-water-water-bong

Copy link
Copy Markdown
Author

Device re-measurement on the rebased build at d20499d08 (HRX0, gfx1151, gpu-hold, llama-bench -dev HRX0 -ngl 99 -p 512 -n 64 -r 3):

check body was (9dd736c19) now at d20499d08
pp512 67.07 ± 1.52 t/s 75.40 ± 2.80 t/s
tg64 3.64 ± 0.19 t/s 4.07 ± 0.02 t/s
GGML_HRX_STRICT=1 placement 383/5113 off (MUL_MAT 281, CONT 89, UNARY 13), 384 splits identical: 383/5113 off (MUL_MAT 281, CONT 89, UNARY 13), 384 splits
greedy /completion ' Paris.\nThe capital of the United States is Washington, D.C.' identical

So the rebase preserved the arch exactly: same off-node set with the same per-op breakdown, same 384 splits (i.e. the same fallback pattern that the body explains), and the same greedy continuation as the pre-rebase run and the CPU/HF reference. The pp/tg difference I am not attributing to the rebase — the earlier run was on a hot box (die 65-75 C) and this one started at 42 C; #194-#197 do not touch these graphs. Treat the placement table and the greedy string as the evidence, the t/s as indicative.

(For completeness: the CPU-fallback explanation in the body, input_size % 256 for the dense MUL_MAT schedules, is still the operative constraint at this base — the relaxation for element-wise weights is a separate PR, #199, not part of this branch.)

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at d20499d08

The rebase is content-neutral and I checked it as a range-diff, not as a size comparison:

$ git range-diff d8bd0ff60..15cb87ebf 428937ae6..d20499d08
1:  6da4ef813 < -:  --------- model: Zamba2 on HRX (Mamba-2 layers + 2 shared attention blocks)
2:  15cb87ebf = 1:  d20499d08 model: Zamba v1 (Zyphra/Zamba-7B-v1)

The = is the point: the single remaining commit is byte-identical to the one I reviewed as READY at 15cb87ebf, and the +142/−17-style drop you will see in the PR counters (+479/−4, 11 files -> +420, earlier +860 lines) is only the Zamba2 commit that is now in the base. #184 is no longer stacked on #178, so the "zamba2 must merge first" constraint is gone and this merges on its own.

Nothing else changed: 11 files, no HRX/HIP/AMD file touched, zamba.cpp/llama-model*.cpp/llama-arch.* plus the GGUF arch strings, and the ZAYA arm untouched.

@bong-water-water-bong bong-water-water-bong added the tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied label Nov 4, 2026

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY on content at d20499d08 — but it now reads CONFLICTING, so it needs a rebase before it can merge.

The content is what I reviewed: git range-diff d8bd0ff60..15cb87ebf 428937ae6..d20499d08 shows one = commit (the Zamba v1 change, byte-identical) with the Zamba2 commit now in the base. What changed since is the base: 1bit/hrx-vulkan-patched has moved under it (#198/#193/#199/#200 are queued onto it), so GitHub cannot merge this branch as it stands.

Nothing to re-review: rebase onto the current 1bit/hrx-vulkan-patched and the verdict above applies to whatever head carries the same commit.

Stacked on the Zamba2 PR: Zamba v1 needs LLM_TENSOR_SSM_MIX / llama_layer::ssm_mix, which that PR adds. Forward-ported
from the removed Vulkan line (1bit/vulkan-upstream @ 5319c0e) onto this base.

The arch: 76 layers, `layers_block_type` alternating `mamba` and `hybrid`. A Mamba-1 layer is RMSNorm -> Mamba-1 ->
residual. A hybrid layer is
  concat(hidden, input embeddings) -> RMSNorm (ATTN_POST_NORM, 2*n_embd wide) -> one shared transformer block (MHA with
  no rotary, k/v length = 2*n_embd/n_head, GEGLU FFN) -> per-layer linear mix (SSM_MIX) -> + residual -> RMSNorm ->
  Mamba-1 -> + residual
and the transformers Mamba-1 mixer is not the plain one: `in_proj` is split into `n_mamba_heads` (2 here) heads, each
with its own `x_proj`/`dt_proj` ({n_heads, dt_rank+2*d_state, head_dim} and {n_heads, head_dim, dt_rank}), so B and C
enter the scan as groups and the matmuls run batched over the head axis. `build_zamba_mamba()` is
`build_mamba_layer()` with those two matmuls replaced. The shared block is stored once in the checkpoint and every
hybrid layer points at the same tensors (`shared_transf` under the first hybrid layer), so the model, the converter and
the saver all have to handle the aliasing: `llama_model_saver` now remembers the tensor objects it wrote and skips a
second visit (a *distinct* tensor with a colliding name still asserts), which is what makes save/load roundtrip pass.

Tensors: A = -exp(A_log) reshaped to {d_state, d_inner}; dt_proj_bias and D flattened from {n_heads, head_dim}; in_proj's
[x, gate] rows de-interleaved (the reference does view(d_inner, 2)); `ssm_x`/`ssm_dt` keep their head axis. The tokenizer
is LlamaTokenizer from tokenizer.json, whose name list carries a [PAD] the embeddings have no row for, so the vocab is
truncated to the model's 32000. `ssm_n_group` is written as the Mamba-1 head count, and the saver restores it from
`ssm_x->ne[2]` because hparams keeps it at 0.

Proof (CPU, GGML_HRX=OFF), Zyphra/Zamba-7B-v1 @ b8c9e7ede2f6:
- test-llama-archs: `zamba ... OK (0.00e+00) ... roundtrip OK` (needs the 2*n_embd key/value length for the
  synthetic model, exactly as Zamba2 does).
- converted by this branch: 784 tensors, 14.5 GB bf16.
- llama-perplexity, calibration window, -c 512 --chunks 1: PPL 6.1107.
- KLD against HF transformers `ZambaForCausalLM` (f32 both sides, the identical 255 positions of the same 512-token
  window): KLD(HF||GGUF) mean 0.0022 (p99 0.0036, max 0.0036), same top-1 100.000 %, total-variation distance 0.0065.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Author

Rebased onto the moved base, at b368b5d69 (was d20499d08). Base is now 16a9bc82c, which carries #183 BlackMamba, #199,
#198 and #201 — that is why it conflicted: two arch ports adding to the same tables.

Content-neutral, by your own method. Own contribution = git diff <merge-base>..<head>, +/- lines only:

before (428937ae6..d20499d08)  67f0cf2a744d8981
after  (16a9bc82c..b368b5d69)  67f0cf2a744d8981

Same hash, so no code line changed — only where the entries sit relative to BlackMamba's. range-diff shows the expected ! (the
BLACKMAMBA lines moved into the base, i.e. context), with every added line identical.

Three conflicts, all "both archs add to the same list", plus the two shapes that bite:

  1. src/models/models.h — the two struct llama_model_* bodies share one trailing };, so resolving the hunk as a choice
    silently deletes a struct. Reconstructed llama_model_blackmamba complete and diffed it against git show 16a9bc82c: to prove
    it, then appended llama_model_zamba after it. Delta against the base is now exactly the 15 added lines of the zamba struct.
  2. tests/test-llama-archs.cpp — the LLM_ARCH_ZAMBA2 arm became LLM_ARCH_ZAMBA || LLM_ARCH_ZAMBA2 rather than being replaced
    (the 2*n_embd shared-attention key/value length applies to both).
  3. src/llama-model.cpp — kept both case LLM_ARCH_BLACKMAMBA: and case LLM_ARCH_ZAMBA: in the rope-NONE group; the other two
    hunks in that file are the mapping case and the ZAMBA2 || ZAMBA recurrent filter, both unchanged from what you reviewed.

The two - lines anywhere in the diff are the two intended widenings (the ssm_n_group saver arm, which now reads the Mamba-1 head
count from ssm_x->ne[2] for ZAMBA, and the arch-test arm above); constants.py and conversion/__init__.py are pure additions and
still ast.parse.

Re-proved on the new base, not just range-diffed (GGML_HRX=OFF, gcc, Release, 439 targets, BUILD_EXIT=0, under thermal-run):

|      blackmamba|AMD RYZEN AI MAX+ 395 w/ Radeon 8060S|   MoE|  OK (0.00e+00)|       OK|
|          zamba2|AMD RYZEN AI MAX+ 395 w/ Radeon 8060S| Dense|  OK (0.00e+00)|       OK|
|           zamba|AMD RYZEN AI MAX+ 395 w/ Radeon 8060S| Dense|  OK (0.00e+00)|       OK|

128 OK rows, no FAIL row (Meta rows are SKIP, no Metal). blackmamba is now the base's own arch, so that row is a regression check
that the five-way merge of the arch tables did not disturb it.

Standing line below; the device numbers in the body were measured at the earlier head and are unchanged by a merge-only rebase (same
graph, same GGUF), and I can re-take them if you want them quoted at this sha.

@bong-water-water-bong

Copy link
Copy Markdown
Author

#184 ready for review at b368b5d

Rebased on 16a9bc82c (which now carries #183 BlackMamba). Own contribution hashes to the same +/- lines as the head you verdicted (67f0cf2a744d8981), conflicts were all "both archs add to the same table", and test-llama-archs on the new base is zamba / zamba2 / blackmamba all OK (0.00e+00).

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at b368b5d69

Rebase-only, and the content is the commit I range-diffed: own contribution hashes to 67f0cf2a744d at this head and at d20499d08 (11 files, +416/−4) — identical, with the base now 16a9bc82c (#201 merged). So git range-diff d8bd0ff60..15cb87ebf 428937ae6..d20499d08's single = commit is what is here too, and the CONFLICTING state my last verdict flagged is gone.

Nothing to re-review: Zamba v1's zamba.cpp / llama-model*.cpp / llama-arch.* plus the GGUF arch strings, no HRX/HIP/AMD file touched, and the ZAYA arm untouched. It is no longer stacked on Zamba2, so it merges on its own.

@bong-water-water-bong
bong-water-water-bong merged commit e9870b3 into 1bit/hrx-vulkan-patched Nov 4, 2026
@bong-water-water-bong

Copy link
Copy Markdown
Author

Placement in the body above is stale — it was measured before fork #199 (the % 32 dense-matmul admission)
merged. Re-measured just now at the merged head c08e9510b, same model (ref/Zamba-7B-v1-bf16.gguf), same
method (GGML_HRX_STRICT=1 GGML_HRX_REPORT=2, one 512-token prefill chunk, gfx1151, gpu-hold):

254 of 5113 nodes off HRX — MUL_MAT x 165, CONT x 89 — 179 graph splits

(was 383: MUL_MAT x 281, CONT x 89, UNARY x 13, 384 splits)

So #199 is worth -116 MUL_MAT and -13 UNARY on this model, CONT untouched, and 205 fewer graph splits. The
model itself is unchanged; nothing about this PR's code or its CPU evidence moves.

The first node STRICT names is the shape class, not a surprise:

GGML_HRX_STRICT=1: "ssm_x_db-0" is MUL_MAT f32 ne=[264,2,2,1] and is not placed on HRX;
254 of 5113 nodes off HRX (MUL_MAT x 165, CONT x 89), 179 splits

ne=[264,2,2,1] is a rank>2 dense matmul (input->ne[2] = 2), which is the bulk of what is left and is
dispatch work, not a gate: the % 32 shapes this PR's graphs still lose are the batched ones, and the % 256
K-quant motif class is separate (engine ggml-org#991 carries both, with the shapes).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant