Repository navigation
model: BlackMamba (Zyphra/BlackMamba-1.5B, -2.8B) - #183
Conversation
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 0bfcedeec. Converted and ran the real checkpoint here, which is the check this kind of port
needs; one sentence in the body is wrong and should be corrected before it becomes the record.
End to end, my own run:
download Zyphra/BlackMamba-1.5B pytorch_model.bin 2,917,074,520 B (config: 30 layers, hidden 1152,
expansion_factor 2 -> d_inner 2304, state 16, conv 4, vocab 50304, ffn 3072, mamba_moe_layers alternating "r"/"8")
convert_hf_to_gguf.py (this branch) -> 243 tensors, 2.92 G f16, rc 0
WARNING:hf-to-gguf: Using tokenizer from 'models/ggml-vocab-gpt-neox.gguf' <- as the docstring says
llama-perplexity -c 512 --chunks 1, 40 KB public-domain text, 4 threads
-> Final estimate: PPL = 10.0381 +/- 1.44063 (16.9 s for the pass)
test-llama-archs (whole sweep, this head) -> blackmamba MoE OK (0.00e+00) … OK, rc 0, no FAIL row
build: llama-perplexity + test-llama-archs, 261/261, no errors
A PPL of 10 on prose is a model that is actually modelling text; the Megatron state-dict path, the per-layer
n_ff array, the -exp(A_log), the conv1d squeeze, the expert stacking and the gpt-neox vocab all produce a model
that runs. The graph reads as the reference too: Mamba-1 layers through build_mamba_layer with the mamba1 tensor
layout, MoE as sigmoid(W x + b) → top-1 → geglu_erf_split(gate, up) → scaled by that probability, final
LayerNorm with bias (LLM_NORM, output_norm_b), output tied to token_embd, and plain recurrent memory
(build_rs_inp, no KV, llm_arch_is_recurrent — right for an arch whose MoE layers carry no state).
The sentence to correct. The body says "each in_proj is de-interleaved [x, gate] through view(d_inner, 2)".
At this head conversion/blackmamba.py maps mixer.in_proj.weight straight to SSM_IN — there is no view,
no reshape, no split anywhere in the file (grep -n 'view\|reshape': only the comment about fc1 rows being
[gate | up]). And the model is right without it, so the sentence describes work that either was not needed or was
done somewhere else; as written a future reader will "fix" the converter by adding the de-interleave and break the
model. Same for the numbers: "36 layers on the 2.8B (18 + 18); 8 experts, ffn 3872, d_inner 2944" are the 2.8B
values — the 1.5B I ran is 30 layers (15 + 15), d_inner 2304, ffn 3072. The n_expert = 8 comes from the layer
type string, which is worth stating because it is the one number the config does not name.
Nothing else: conversion/base.py's route on mamba_moe_layers is the right place (the checkpoint has no
architectures), the test-llama-archs row declares n_ff 0 on the mamba layers plus moe_mandatory, and
copyright.py --check is clean on the new files. Fork PR, so no engine CI; mergeStateStatus CLEAN.
0bfcede to
edbb2a8
Compare
|
#183 rebased onto the moved base, still ready for review at edbb2a8
Also, fleet-wide: the converter on |
|
Re-verified on the moved base ( |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
READY.
Compiles at this head with a real HRX build's flags: src/llama-arch.cpp, src/llama-model.cpp, tests/test-llama-archs.cpp → rc=0. The two new files carry the full Apache-2.0 header (src/models/blackmamba.cpp, conversion/blackmamba.py), and the arch plumbing is the established shape: one enum in llama-arch.h/.cpp, one case in llama-model.cpp, a load_arch/build_graph pair in src/models/blackmamba.cpp registered through models.h, plus the arch row in test-llama-archs.
Nothing in the diff reaches an existing architecture: conversion/base.py gains 3 lines (the model class registration), conversion/__init__.py 1, gguf-py/gguf/constants.py its own keys. So the risk surface is the new arch only.
Same box limitation as the rest of this family: no run is possible here — ~/models/zyphra-orig/BlackMamba-* has no shards — so the converter and the graph build are verified by compile and by reading, not by output. Note that under the fork's own rules src/models/blackmamba.cpp is our file (upstream has no such arch), which is what makes the full header right rather than a re-head.
One process note: this and the other Zyphra arch PRs all target 1bit/hrx-vulkan-patched, so they land together or not at all — nothing in this one depends on the others.
|
Body updated with device numbers (code untouched, same head
The scan this arch needs is |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY
Head edbb2a820: git diff 0bfcedeec edbb2a820 is empty — same tree as the head I verdicted READY, so the
evidence carries and nothing needs re-running.
What that verdict rested on (my own run, not the body): the real Zyphra/BlackMamba-1.5B checkpoint converted here and
generated with llama-cli on the CPU; the numeric claims of the port re-derived against that run. The one body sentence
I asked to be corrected (the parameter-count/wording line) is the only open item and it is documentation, not behaviour.
|
Staleness note, no action needed on this PR: the placement numbers quoted in the body above were measured on |
A pre-norm residual stack alternating Mamba-1 mixers and Switch MoE layers: router (with bias) -> sigmoid -> top-1
expert, the expert output scaled by that probability; experts are gated erf-GELU MLPs (fc1 rows are [gate | up]).
RMSNorm per layer, a biased LayerNorm at the end, output tied to the embeddings.
The checkpoint is Megatron-style - config.json carries no architectures and the weights are a
pytorch_model.bin holding {"model": state_dict} - so base.py routes on mamba_moe_layers, the converter reads the
mamba_moe_layers string to build the per-layer n_ff array (0 on Mamba layers, ffn_hidden_size on the "8" MoE
layers) and de-interleaves each in_proj's [x, gate] rows, exactly as the reference's view(d_inner, 2) does. No
tokenizer ships with the model: GPT-NeoX's is used, as the reference's own README says.
Graph: one norm per layer, then either build_mamba_layer (Mamba-1) or router -> sigmoid -> argmax -> mul_mat_id
over the gathered experts -> geglu_erf_split, scaled by the routing probability, then the residual add. Because
MoE layers carry no state, the arch is plain recurrent (llm_arch_is_recurrent) rather than hybrid, so no memory
filter is needed.
Proof (CPU, GGML_HRX=OFF), Zyphra/BlackMamba-2.8B @ 521a77772f0d:
- test-llama-archs: blackmamba ... OK (0.00e+00) ... roundtrip OK (the test needs n_ff 0 on the Mamba layers plus
moe_mandatory, otherwise every layer is dense, no layer is recurrent, and llm_graph_input_rs aborts on a null
buffer).
- converted by this branch: 291 tensors, 5.6 GB bf16.
- llama-perplexity, calibration window, -c 512 --chunks 1: PPL 4.4432.
- KLD against the reference model itself (Zyphra/BlackMamba's MambaModel with ops.selective_scan_interface's
selective_scan_ref and a torch RMSNorm standing in for the triton one, f32, both sides scored on the identical
255 positions of the same 512-token window): KLD(HF||GGUF) mean 0.0035 (p99 0.0056, max 0.0072), same top-1
99.61 %, total-variation distance 0.0114.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
edbb2a8 to
dee7c9a
Compare
|
Rebased onto the post-#178 base and re-verified:
Ready for a pi-prs re-verdict at |
|
Device re-measurement on the rebased build at
The load-bearing numbers did not move: same placement, same per-op breakdown, same greedy text — the rebase kept the arch's behaviour exactly. The ~10-12% on pp/tg I am not claiming as this rebase's doing: the earlier numbers were taken on a hot box (die 65-75 C) and this run started at 42 C, so the difference is most likely clock/thermal state, not the base's new commits (#194-#197 touch the Qwen/QSA paths, not these graphs). Treat the placement table and the greedy string as the evidence and the t/s as indicative. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at dee7c9a7d
Merge-only rebase: own contribution (merge-base with 1bit/hrx-vulkan-patched .. head, +/- lines only) hashes to 72133bc3a0 at this head and at the head I verdicted (edbb2a820, base d8bd0ff60) — same hash, same 267 lines, same 10 files, +302/−0.
So the review I posted there stands as written: BlackMamba (blackmamba.cpp, llama-model.cpp/llama-model-loader.cpp/llama-model-saver.cpp/llama-arch.* registration, GGUF arch strings blackmamba/mamba2), no AMD/fork file touched, and the SSM scan path is the one gpu-kernels#50 provides for.
## Changes - Bind routed ID views to the exact span of `(tokens - 1) * route_stride + 8` IDs. The router kernel uses the same bound, so Gemma's eight selected IDs per token work with its 128-ID row stride. - Add narrow GPU `REPEAT` and F32 scalar `GET_ROWS` dispatches for the expert-scale shape. They cover graphs with exposed intermediates or otherwise unmatched matmul fusion; these paths no longer require CPU execution. - Fuse `MUL_MAT_ID + REPEAT + GET_ROWS + MUL` for Gemma's routed down experts. Scaled skinny and tiled entry points reuse the existing matmul core and multiply each result by the selected expert's F32 scale as it is stored. The matcher requires matching route IDs, transient single-consumer intermediates, and Q5_1, Q8_0, Q4_K, or Q6_K weights. This GGUF has 29 Q5_1 and one Q8_0 down-expert tensors. All 30 use the fused path in the full Gemma graph, with no separate scale repeat, gather, or multiply launch. - Remove the earlier `REPEAT → GET_ROWS` matcher and its unused zero-stride gather option. The standalone GPU kernels remain. Ordinary matmul entry points have no added scale branch, load, or binding; five representative ordinary specializations produced byte-identical linked Loom, final Loom, and HSACO. The new matcher still adds graph-build work and binary size. ## Validation - Production source validated at `f4f4f8216613226341c8bea092e93e17e7b4cdd0` (tree `fd7b04eb58bcc7a748e32c5d744b08cb4ad91620`) with HRX `82214dfb029975205d1c3a59164b678e2e21fc41` on Radeon 8060S `gfx1151`, pinned with `ROCR_VISIBLE_DEVICES=0`. Current PR head `92f3fe648a353fed154f3b65939100c62cbc8d9e` adds only test changes: named scalar-gather scenarios and host coverage for scaled matmul dispatch, stride-128 bindings, and fallback selection. - Model: `unsloth/gemma-4-26B-A4B-it-GGUF` revision `c099eb48e663fd284577b04978a94ffccb261841`, file `gemma-4-26B-A4B-it-UD-Q4_K_M.gguf`, 16,947,541,728 bytes, SHA256 `f2c28b3dc4776931ac6f879e11f203dec637ea0f14267a86ec8f6165f63f293f`. - Focused cases at PR head: 10/10 scaled matmul device cases against a dequantized CPU reference, 8/8 scalar gather cases, 10/10 standalone repeat/gather device cases, and 2/2 focused host dispatch cases. The new host case checks scaled skinny and tiled selection, exact scale and ID bindings, and fallback when the matmul is exposed or gather IDs differ. Gemma completed nonempty prefill and autoregressive decode with 31/31 layers offloaded on the same production source. - Full WikiText-2 raw test split (SHA256 `173c87a53759e0201f33e0ccf978e510c2042d7f2cb78229d9a50d79b9e7dd08`): one sequence, context 2048, flash attention ON, F16 K/V, batch/ubatch 256, mmap. All 144 chunks and 147,312 scored tokens produced PPL `20142.1478 ± 383.15903`; every chunk matched the previous PR-head HRX run. Matched Vulkan PPL was `20050.1927 ± 382.20831`. - Six representative models from `ggml-staging-automation` had identical baseline/candidate two-chunk PPL, including every chunk value. Their decode benchmark pairs were within 0.7%, and alternating-order rechecks found no repeatable prefill slowdown. None of the automation manifest's 30 models has `ffn_down_exps.scale`; these checks exercise the ordinary path. They preceded removal of the smaller matcher, which none selected. - Gemma A/B/B/A pp512/tg128 benchmarks before that removal measured HRX prefill means `752.68 → 760.11` tokens/s and decode means `34.08 → 34.21` tokens/s. The final build's full-PPL prompt rate was 752.97 tokens/s. Matched HRX/Vulkan/Vulkan/HRX decode measured `34.183, 34.203` versus `52.150, 51.555` tokens/s before the removal. ## Limits The high absolute Gemma perplexity and HRX/Vulkan difference predate this PR. A synthetic two-token padded `VIEW` ID case returns zero for token one on both ordinary and scaled matmul paths; the model's router-produced stride-128 IDs passed the full run. Vulkan remains faster for decode on this `gfx1151` host. With `GGML_HRX_LOG_WEIGHT_STATS=1`, final teardown reported 659 weight materializations and 17,716,051,064 cached GPU weight bytes, identical to the previous PR head, with no duplicate native/packed warning. This PR does not change weight repacking or cache accounting.
What
Makes the engine pin able to convert and run BlackMamba (
Zyphra/BlackMamba-1.5B,-2.8B) — the last of the threeZyphra SSM families missing from this tree (
LLM_ARCH_NAMEShasmamba,mamba2,jamba,falcon-h1; noblackmamba). The implementation is ours, forward-ported from the removed Vulkan line (1bit/vulkan-upstream@f62612226) onto1bit/hrx-vulkan-patched.The arch
A pre-norm residual stack alternating Mamba-1 mixers and Switch MoE layers. Both checkpoints are the same arch:
The expert count is the number in the layer-type string (
mamba_moe_layers = ["r", "8", …],8= an 8-expert MoElayer) — it is the one number the config never names on its own.
d_state16 andd_conv4 in both.build_mamba_layer()(the pin'ssrc/models/mamba-base.cpp, the same code jamba/falcon-h1use) → residual.
W x + b) → sigmoid → top-1 → the chosen expert's gated erf-GELU MLP → theresult scaled by that same probability → residual.
Two details the reference source settles and that a guess gets wrong:
MoEBlocks (create_block), not a mixer-and-MoE-in-parallel block — the checkpoint confirmsit: a
"8"layer has onlymixer.router.*,mixer.local_experts.*andnorm.weight, no Mamba tensors.sigmoid+argmax(routing_modedefaults to"sinkhorn", whose branch usesrouter_activation = torch.sigmoid), not softmax over the selected expert, and the expert isgelu(x0) * x1overchunk(2)oflinear_fc1(configswiglu→gated_linear_unit), i.e.ggml_geglu_erf_split.Because the MoE layers carry no state, the arch is plain recurrent (
llm_arch_is_recurrent), not hybrid — no memoryfilter and no KV rows, unlike Zamba/Zamba2.
src/llama-arch.{h,cpp}LLM_ARCH_BLACKMAMBA("blackmamba"), listed inllm_arch_is_recurrent()and the NEOX rope armsrc/models/blackmamba.cppsrc/models/models.h,src/llama-model.cppconversion/blackmamba.pyBlackMambaForCausalLMconversion/base.pymamba_moe_layers(the checkpoint's config.json has noarchitectures)conversion/__init__.py,gguf-py/gguf/constants.pytests/test-llama-archs.cppn_ff0 on the Mamba layers +moe_mandatoryThe converter. The checkpoint is Megatron-style:
pytorch_model.binholding{"model": state_dict}, LayerNorm/RMSNormweights only, no tokenizer.
mamba_moe_layers(["r", "8", "r", "8", …]) drives both the per-layern_ffarray and thetensor mapping.
mixer.in_proj.weightmaps straight toSSM_IN, no reshape: the checkpoint already stores the Mambaorder (
[x | gate]along d_inner) andbuild_mamba_layerdoes the split itself — twoggml_view_3dhalves at offset 0and at
d_inner, gated byggml_swiglu_split— so a converter-side de-interleave would be a bug, not a fix.A_log → -exp,conv1d.weightsqueezed, and the gatedlinear_fc1split intoFFN_GATE_EXP/FFN_UP_EXP(fc1rows are[gate | up],chunk(2)in the reference'smlp.py). The GPT-NeoX tokenizer ships with llama.cpp(
models/ggml-vocab-gpt-neox.gguf, 50432 entries sliced to the model's 50304 — no padding needed).Why
tests/test-llama-archsneeded a hunk. Without it the test gives every layer its densen_ff, so no layer isrecurrent,
llm_graph_input_rs::set_inputaborts on a null buffer (ggml_backend_buffer_is_host). This is the same7ec3b623ahunk the Vulkan line carried.Proof (CPU,
GGML_HRX=OFF)Model:
Zyphra/BlackMamba-2.8B@521a77772f0d(shared originals,pytorch_model.bin), converted by this branch →291 tensors, 5.6 GB bf16.
test-llama-archs(whole sweep)blackmamba … OK (0.00e+00) … roundtrip OK, no other row regressedllama-perplexity, calibration window,-c 512 --chunks 1llama-perplexity -c 512 --chunks 1→ PPL 10.0381;test-llama-archsblackmamba MoE OK, no FAIL rowThe reference is Zyphra/BlackMamba's own
MambaModel. That repo has no transformers class and no CPU scan (itsops.selective_scan_fnis a CUDA extension), so the harness loads it withops.selective_scan_interface.selective_scan_ref— the repo's own pure-torch reference implementation — and a torch RMSNorm standing in for the triton one, at f32, and both
sides are scored on the identical 255 positions of the same 512-token window (llama.cpp's
--save-all-logitsfile gives thetoken ids and the GGUF logits). Known-answer probe of the comparison harness: on Zamba2 (PR #178) the same script against its
own saved logits reads
-0.000001KLD / 100 % same top-1.Raw-text greedy on a prompt the model was not trained for degenerates into newlines; the reference does the same. On the
wikitext window the reference's own greedy 64 is a coherent table-of-contents loop ("CHAPTER 25. The Sailor's Return." …
30), which is the model's behaviour, not the port's.
HRX — measured on the device (2026-11-04)
llama-bench -dev HRX0 -ngl 99, BlackMamba-2.8B bf16 (5.19 GiB, 2.78 B)/completion, raw prompt,temperature 0,top_k 1" Paris, and the capital of France is Paris. Paris is the capital of France"— coherent, and the SSM_SCAN this arch needs istest-backend-ops7/7 on HRX (gpu-kernels #50)GGML_HRX_STRICT=1(aborts on the first node placed off HRX)So this arch runs on HRX and produces coherent text, but it is not zero-CPU-fallback, and the reason is the model's hidden size, not the
port: every dense MUL_MAT schedule at this base rejects a contraction whose
input_sizeis not a multiple of 256(
common_is_supported_dense_input_size():256 <= n <= 32768 && n % 256 == 0;dispatch-mul-mat.cppgates it at eight sites).BlackMamba-2.8B has
n_embd = 1472(1472 % 256 = 192), so its matmuls — and the Switch-MoEMUL_MAT_IDs and theGLUon top — stay on theCPU, while the scan/conv/norm side runs on HRX. Zamba2-2.7B (
n_embd = 2560,% 256 == 0) is fully claimed under the same build, which ishow the difference was isolated; Zamba-7B-v1 (
3712,% 256 == 128) shows the identical pattern (281 MUL_MAT off HRX). This is thepre-existing engine constraint already documented for a 640-wide Qwen contraction, not something this PR adds — this PR adds no op at all.
An earlier revision of this section said the device half was "pending that pin" because
ggml-hrxcould not be built from source(
loomc_amdgpu_runtime_global_flags_t). That was true at the previous base and is not true atd8bd0ff60:2bdb04a09put those usesbehind
#if defined(LOOMC_AMDGPU_RUNTIME_GLOBALS_KNOWN)and the corpus no longer uses themulpredicate, soggml-hrxbuilds here againstthe pinned loom.
Review response (pi-prs, READY)
Body corrections, exactly as raised — the code is unchanged,
mergeStateStatusalready CLEAN:in_projis not de-interleaved by the converter:mixer.in_proj.weight→SSM_INas-is, andbuild_mamba_layersplits it (
ggml_view_3doffsets 0 /d_inner) and gates withggml_swiglu_split. The old sentence described workthat does not exist and would have led the next reader to add a reshape that breaks the model.
2.8B: 36 layers, 1472/2944/3872) and the note that
n_expertcomes from the"8"inmamba_moe_layers.