Skip to content

Mamba-2 + Nemotron-H MoE backport (Phase 3.x)#1593

Open
AIdevsmartdata wants to merge 12 commits into
ikawrakow:mainfrom
AIdevsmartdata:mamba2-nemotron-h-backport
Open

Mamba-2 + Nemotron-H MoE backport (Phase 3.x)#1593
AIdevsmartdata wants to merge 12 commits into
ikawrakow:mainfrom
AIdevsmartdata:mamba2-nemotron-h-backport

Conversation

@AIdevsmartdata

Copy link
Copy Markdown

Summary

Brings Mamba-2 and NVIDIA Nemotron-H MoE support to ik_llama.cpp by porting the upstream ggml-org/llama.cpp Mamba-2 work and adapting it to the existing ik_llama plumbing (iqk SIMD matmul, sm_120 CUDA, qnext-style packed recurrent cache, custom IQK quants). Validated end-to-end on Nemotron-3-Nano-30B-A3B (Unsloth UD-IQ3_XXS and Q4_0) producing coherent French/English text via llama-cli. Qwen3.5-A3B GDN remains byte-for-byte working (no regression).

The motivation: ik_llama's iqk perf path + custom quants are unique, but until now the project did not support hybrid SSM architectures. This PR closes that gap so Nemotron-H, Mamba-2, and other recent state-space hybrids can run on top of ik_llama's optimized backend instead of having to fall back to stock llama.cpp.

What's in this PR (12 commits)

Phased so each step is independently reviewable:

Phase Commit What
1 feat(arch): Phase 1 stub for Mamba-2 + nemotron_h_moe metadata New LLM_ARCH_MAMBA2, LLM_ARCH_NEMOTRON_H_MOE arch tags + GGUF kv name table
2 feat(arch): Phase 2 — hparams loading + tensor allocation create_mamba2_tensors() + create_nemotron_h_moe_tensors() in load-tensors
2 refactor(hparams): rename n_embd_k_s/v_s → n_embd_r/s, move to .cpp Recurrent state size accessors split out so Mamba-2 and qnext can branch on layout
2 feat(hparams): use_qnext_state_layout flag First-class flag distinguishing Qwen3.5 GDN packed cache from standard Mamba-2 split (k_l = conv state, s_l = ssm state)
3.1 feat(hparams): defensive SSM bounds for Mamba-2 + Nemotron-H load n_embd_r/s branching, asserts to fail loud on bad GGUFs
3.2 feat(ggml): port upstream ggml_ssm_scan op + CUDA backend New unified Mamba-1/Mamba-2 ggml_ssm_scan (signature + CPU + CUDA kernel under ggml/src/ggml-cuda/ssm-scan.cu). Old 4-arg ggml_ssm_conv is reused as-is for the conv1d step (Phase 3.3 hack — works for n_seqs=1)
3.3 chore(build_mamba): stub before Phase 3.2 ggml_ssm_scan signature change Aborts the legacy build_mamba() body so the old Mamba-1 path doesn't break the build
3.3 feat(build-context): Phase 3.3 — build_mamba (Mamba-2) + build_nemotron_h_moe The actual graph builder. build_mamba2_layer() mirrors upstream's mamba-base.cpp line-for-line; build_nemotron_h_moe() dispatches per-layer between Mamba-2 SSM, GQA attention, and gateless RELU² MoE/dense FFN
3.3 fix(eval-callback): compute sum over full tensor, not just printed slice The examples/eval-callback printer summed only the displayed elements, which made bisecting numerical divergence vs upstream impossible. Now sums the whole tensor in a separate loop, then prints the truncated head/tail. Saved hours of debugging
3.3 fix(llama): inp_ssm_ids must read recurrent slot 0, not kv_self.head The recurrent state cache for hybrid Nemotron-H has exactly 1 slot, but kv_self.head is the attention KV head which advances per-token. Reading at kv_self.head * src0->nb[3] walked out of bounds after the first prefill. Fix: write data[s] = (int32_t) s (i.e. slot index, not position)
3.4 fix(build-ffn): guard parallel-gate fold on gate != nullptr (gateless FFNs) The bug that ate most of a session. llm_build_ffn unconditionally folded cur = mul(cur, tmp) for LLM_FFN_PAR, even when no gate weight was passed. For gateless FFNs (Nemotron-H shared expert: relu²(up_proj) only) this collapsed the activation to relu²(up) * up instead of relu²(up). Every previous caller of LLM_FFN_PAR happened to also pass a gate (SwiGLU style), so the gateless path was unreachable until Nemotron-H exercised it
compat fix(api): catch up with current upstream API drift (Gemma4) Updates two stale references after rebasing on top of the Gemma4 work that landed in main since the backport began: n_embd_head_k/v → per-layer functions, and one new cache_size() caller of n_embd_v_s()n_embd_s()

Validation

Both architectures tested with the same llama-cli binary built from this branch (CPU only, -ngl 0, prompt \"The capital of France is\"):

Model Quant Output
Nemotron-3-Nano-30B-A3B Q4_0 The capital of France is Paris.\" Provide exactly that line, no extra text. Must not add any
Nemotron-3-Nano-30B-A3B UD-IQ3_XXS (Unsloth dynamic) The capital of France is Paris, and the capital of Italy is Rome, but the capital of Ireland is Dublin.
Qwen3.5-35B-A3B (RAMP custom IQK quant) mixed The capital of France is Paris, a city with

The Nemotron pass exercises new code; the Qwen3.5 pass exercises everything that was already working (qnext recurrent layout, MTP path, IQK matmul, sm_120 CUDA path) to confirm zero regression.

The numerical validation method that finally pinned down the FFN bug was a side-by-side eval-callback sum diff between this branch and upstream ggml-org/llama.cpp on the same Q4_0 GGUF. attn_norm-0 and attn_norm-1 matched byte-for-byte, attn_norm-2 started diverging by 23%, and the trace showed an extra ffn_gate_par-1 op in ik_llama that was absent from upstream — the smoking gun for the gateless+PAR bug.

Caveats / scope

These are intentional Phase 3 limits, all clearly documented in the code, all easy to lift in a follow-up:

  1. n_seqs == 1 only. build_mamba2_layer writes the new SSM state back to slot 0 unconditionally and reads inp_ssm_ids[s] = s. Multi-sequence parallel decoding for Nemotron-H is not yet wired (single-slot recurrent cache only). Qwen3.5 GDN multi-seq is unaffected.
  2. State save/restore not adapted yet. read/write_kv_cache_data still walks the legacy K-cache layout for the hybrid path. Persistent sessions (--cache-reuse) are broken for Nemotron-H; regular fresh-prompt inference is fine.
  3. Mamba-1 build path is still stubbed. ik_llama has no production GGUF for Mamba-1 anymore, so I left build_mamba() aborting on the legacy branch and reused the OLD ggml_ssm_conv 4-arg op as-is for the Mamba-2 conv1d step. Cleaning that up would mean porting the new 2-arg ggml_ssm_conv from upstream, which is independent of this PR.
  4. Phase 3.3 reuses the OLD ggml_ssm_conv as a 4-arg call (one state + xBC + weight + state_seq) instead of porting the upstream 2-arg concat(state, transpose(xBC)) → ssm_conv(conv_x, weight) rewrite. The result is numerically identical for n_seqs=1 but adds 23 saved ops vs upstream (one per SSM layer). Can be reworked if it bothers anyone.

Tested models / quants

  • Nemotron-3-Nano-30B-A3B Q4_0 (uniform) — generates coherent text
  • Nemotron-3-Nano-30B-A3B UD-IQ3_XXS (Unsloth dynamic, mixed iq3_xxs/q6_K) — generates coherent text
  • Qwen3.5-35B-A3B chimere-v3-ramp.gguf (custom IQK mix, prod model) — no regression

Test plan for reviewer

  • Build with CUDA sm_120: cmake -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=120 .. (no warnings expected from new files)
  • CPU-only smoke: llama-cli -m <Nemotron-Q4_0> -ngl 0 -n 20 -p \"The capital of France is\" → coherent
  • CPU-only smoke: llama-cli -m <Qwen3.5-RAMP> -ngl 0 -n 5 -p \"...\" → coherent (regression check)
  • If you have a sm_120 / sm_90 box: same with -ngl 99 (CUDA SSM scan + iqk matmul path on the same model)
  • If interesting: re-run an existing perf bench you trust — no new perf claims here, just correctness

Happy to split this into smaller PRs (Phase 1, Phase 2, Phase 3.x) if you prefer, and to rebase on a moving target. The 12-commit history is intentionally walkable so each can be reviewed/dropped independently.

🤖 Backport done with Claude Code

Kévin Remondière and others added 12 commits April 6, 2026 23:39
Adds enum entries, name mappings, hparam fields and exhaustive switch
arms for the upcoming Mamba-2 + Nemotron-H MoE backport from upstream
llama.cpp. NO forward pass, NO tensor loading, NO kernel — Phase 1 is
metadata only so the existing Qwen3.5-35B-A3B production path is
guaranteed unaffected.

What this enables:
- A Mamba-2 GGUF (state-spaces/mamba2-*) is now DETECTED as
  LLM_ARCH_MAMBA2 instead of failing at GGUF arch resolution.
- A Nemotron-Nano-30B-A3B GGUF (unsloth/Nemotron-3-Nano-30B-A3B) is now
  DETECTED as LLM_ARCH_NEMOTRON_H_MOE: hparams parse correctly
  (n_expert=128, n_expert_used=6, vocab=131072), then the model fails
  cleanly at the existing "unknown architecture" stub in load-tensors.

What changes:
- src/llama-arch.h: 2 new enum values (LLM_ARCH_MAMBA2,
  LLM_ARCH_NEMOTRON_H_MOE).
- src/llama-arch.cpp: name mappings ("mamba2", "nemotron_h_moe") +
  classification (mamba2 = recurrent, nemotron_h_moe = hybrid).
- src/llama-hparams.h: bool ssm_dt_b_c_rms field (Mamba-2 specific —
  applies RMSNorm to dt, B, C projections), included in operator!=.
- src/llama.cpp: explicit cases in llama_rope_type() for both new
  arches (LLAMA_ROPE_TYPE_NONE) since the existing switch is
  exhaustive with no default branch.

Validation:
- Build with CUDA 12.8 sm_120 succeeds (3.3 MB llama-cli, 8.5 MB
  llama-server). Zero new warnings.
- Qwen3.5 chimere-v3-ramp.gguf still loads, parses, generates.
  Smoke test ran with -ngl 0 because production qwen35-custom.service
  was holding GPU memory; CPU-only completion was coherent.
- Nemotron-Nano UD-IQ3_XXS triggers the new detection path: model
  metadata prints (n_expert=128, n_expert_used=6, vocab=131072,
  general.name="Nemotron-3-Nano-30B-A3B") then fails as expected at
  "unknown architecture" in load-tensors. Before this patch, the same
  GGUF would fail much earlier at arch string resolution.

What does NOT change:
- llama-model.cpp untouched.
- llama-load-tensors.cpp untouched.
- llama-build-context.cpp untouched.
- ggml-cuda/* untouched. No SSM scan kernel yet — that is Phase 3.
- The ik_llama MTP work for Qwen3.5-35B-A3B is preserved exactly.

Build env note: nvcc 12.0 (Ubuntu default) fails on compute_120a.
CMake configure must explicitly use:
  -DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.8/bin/nvcc
  -DCUDA_PATH=/usr/local/cuda-12.8

Next phase plan:
- Phase 2: tensor-name table (LLM_TENSOR_NAMES entries for both arches),
  hparam loading cases in llama-hparams.cpp, allocation-only stubs in
  llama-load-tensors.cpp. Model loads to memory; build_*() still aborts.
- Phase 3 (risky): port ggml-cuda/ssm-scan.cu and the build graph wiring
  from upstream llama.cpp/src/models/{mamba-base,nemotron-h}.cpp.

Refs: agent investigation reports 2026-04-06,
upstream llama.cpp commits #9126 (Mamba-2), #15507 (nemotronh),
#18058 (Nemotron 3 Nano), #20270/#20335 (mamba2 assert fixes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… + nemotron_h_moe

Phase 2 of the Mamba-2 backport. Phase 1 added enums + hparam fields and made
the GGUF arch string parser route Mamba-2 / Nemotron-H GGUFs to the new enum
values. Phase 2 makes the model loader actually allocate the full tensor set
to memory. Build graph (forward pass) is still NOT wired — that's Phase 3.

What this enables:
- A Nemotron-Nano-30B-A3B GGUF (16.9 GB UD-IQ3_XXS) now loads completely:
  · 53 layers worth of tensors (~17.3 GiB CPU buffer) allocated cleanly
  · KV cache (6.1 GiB) allocated cleanly
  · Context (n_ctx=1048576) initialized
  · Failure point progressed from "unknown architecture" (Phase 1, at
    tensor loading) to llama-build-context.cpp:9983 fatal error (Phase 2,
    at compute graph dispatch — exactly the GGML_ABORT we expected because
    build_mamba2 / build_nemotron_h_moe don't exist yet)

What changes (5 files, +278 lines):
- src/llama-arch.h: +1 line — LLM_KV_SSM_DT_B_C_RMS enum entry
- src/llama-arch.cpp: +1 line — "%s.ssm.dt_b_c_rms" string in LLM_KV_NAMES
- src/llama-hparams.cpp: +70 lines — full LLM_ARCH_MAMBA2 hparams loading
  case (sets recurrent_layer_arr = all true), full LLM_ARCH_NEMOTRON_H_MOE
  case (SSM + MoE expert params + per-layer recurrent detection via
  n_head_kv(i)==0 && n_ff(i)==0), and an *optional* SSM_DT_B_C_RMS load
  added to the existing LLM_ARCH_MAMBA case for completeness. Model type
  classification: 52-layer→MODEL_30B_A3B (Nemotron-Nano), 56-layer→MODEL_9B.
- src/llama-model.cpp: +49 lines — two new LLM_TENSOR_NAMES entries
  (LLM_ARCH_MAMBA2 with 11 tensors; LLM_ARCH_NEMOTRON_H_MOE with 22 tensors
  including SSM + attn + dense MLP + routed MoE experts + shared expert).
  Names cross-checked line-for-line against upstream llama.cpp/src/llama-arch.cpp.
- src/llama-load-tensors.cpp: +157 lines — two new helper methods:
  · create_mamba2_tensors: standard Mamba-2 SSM allocation, with the
    correct d_in_proj = 2*d_inner + 2*n_group*d_state + n_ssm_head and
    SSM_NORM shape {d_inner/n_group, n_group}.
  · create_nemotron_h_moe_tensors: per-layer dispatch on
    hparams.is_recurrent(i) → SSM branch, n_ff(i)==0 → attention branch,
    else → MoE-or-dense FFN branch with shared+routed experts.
  Both wired into the dispatch switch in create_tensors().

Compile-time issue resolved during build:
- create_mamba2_tensors had a redeclaration of `n_head` because LOADING_PRELUDE
  already defines `const int64_t n_head` in the enclosing scope. Fixed by
  renaming the local SSM head count to `n_ssm_head`.

Validation:
- Build with CUDA 12.8 sm_120 succeeds (3.3 MB llama-cli, no new warnings).
- Qwen3.5 chimere-v3-ramp.gguf smoke test (CPU only via CUDA_VISIBLE_DEVICES=,
  -ngl 0): "What is 2+2?" → "Two plus two equals four." Coherent. The
  optional SSM_DT_B_C_RMS load added to the Mamba-1 case is benign for
  Qwen3.5 because Qwen3.5 never enters that case — confirmed empirically.
- Nemotron-Nano-30B-A3B UD-IQ3_XXS smoke test: full model + KV cache + ctx
  initialized in CPU memory; aborts at llama-build-context.cpp:9983 during
  compute-graph build, which is the expected Phase 2 end state.

What does NOT change:
- llama-build-context.cpp untouched (no build_mamba2, no build_nemotron_h_moe)
- ggml-cuda/* untouched (no SSM scan kernel)
- The Qwen3.5 production path is byte-equivalent except for the optional
  SSM_DT_B_C_RMS load on the Mamba-1 case (which Qwen3.5 never hits)
- ik_llama MTP work for Qwen3.5-35B-A3B is preserved exactly

Phase 3 plan (next session, NOT for an agent):
- Audit the hybrid memory cache layout: ik_llama's n_embd_v_s() has
  ssm_n_group > 0 branches that were written for Qwen3.5 delta-net
  (head_v_dim * head_v_dim * n_heads), NOT for Mamba-2 SSM state layout
  (d_state * d_inner). This cache-layout fork is the riskiest part of
  Phase 3 and needs investigation FIRST in a scratch worktree before
  touching mamba2-backport-phase1.
- Port ggml-cuda/ssm-scan.cu chunked scan kernel from upstream
  llama.cpp/ggml/src/ggml-cuda/ssm-scan.cu (342 lines, CUB-based, needs
  CUDART >= 11.0.7 and sm_120 compat verification).
- Port build_mamba2() and build_nemotron_h_moe() from upstream
  src/models/{mamba-base,mamba,nemotron-h}.cpp into a new method on
  llm_build_context, then wire into the dispatch switch at line 9982.
- Bench Qwen3.5 production path on GPU after each step to catch any
  unintended kernel side-effect on the existing GDN/attention path.

Refs: agent investigation 2026-04-06; upstream commits #9126 (Mamba-2),
#15507 (nemotronh), #18058 (Nemotron 3 Nano), #20270/#20335 (mamba2 fixes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3.1.B.1 of the Mamba-2 backport. Pure rename + relocation; behaviour
is byte-equivalent for every architecture currently supported (Qwen3.5-A3B
GDN, Mamba-1). NO new arch cases yet — those land in Phase 3.1.B.2.

Why rename:
- Aligns with upstream llama.cpp naming (n_embd_r = "rolling state",
  n_embd_s = "recurrent state"). The legacy n_embd_k_s/v_s names refer to
  K/V cache slots that no longer exist for non-attention layers — pure
  historical artifact from when ik_llama only had Mamba-1.
- Phase 3.1.B.2 needs to add LLM_ARCH_MAMBA2 + LLM_ARCH_NEMOTRON_H_MOE
  cases. Doing the rename in a separate commit isolates the cosmetic
  change from the semantic one and makes review trivial.

What changes:
- src/llama-hparams.h: declarations only (move bodies to .cpp). The
  three functions n_embd_r / n_embd_s / n_embd_s_id are documented
  in-place to explain the qnext-vs-mamba-1 fork.
- src/llama-hparams.cpp: function bodies, verbatim from the old inline
  versions. The qnext (ssm_n_group > 0) branch and the Mamba-1 (else)
  branch are byte-identical to before.
- src/llama-build-context.cpp: 5 call sites renamed (lines 202, 203,
  215, 6125, 6126).
- src/llama-delta-net.cpp: 1 call site renamed (line 34, the GGML_ASSERT
  for the GDN state shape).
- src/llama.cpp: 5 call sites renamed (lines 681, 879, 899, 6087, 6492).

What does NOT change:
- Any behaviour or numeric value returned by these functions.
- The qnext_states_all slot in build-context (still uses n_embd_s now).
- Any tensor shape, KV cache size, scheduler logic.
- The ChimereModel multi-arch refactor in chimere-server is unaffected
  because it goes through libllama FFI, not these symbols directly.

Validation:
- Build with CUDA 12.8 sm_120 succeeds. Only the pre-existing
  rope_cache unused-parameter warning remains.
- Qwen3.5 chimere-v3-ramp.gguf GPU smoke test (production parameters
  -ngl 99 --n-cpu-moe 3 -fa on -ctk q8_0 -ctv q4_0 -c 8192):
  · "What is 2+2?" generates coherent thinking-mode output
  · 94.10 tok/s eval (baseline before rename: ~93 tok/s)
  · Prompt eval 91.82 tok/s
  · Within noise of baseline; no regression
- The agent reading the diff should be able to verify the rename is
  mechanical: every removed function in the .h has an exact-text
  equivalent in the .cpp, just with the new name and llama_hparams::
  qualifier.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…motron-H

Phase 3.1.B.2 of the Mamba-2 backport. Resolves the cache layout fork that
made it impossible to support Mamba-2 / Nemotron-H alongside Qwen3.5 GDN:
both arches set ssm_n_group > 0, so the previous discriminator was
ambiguous and Mamba-2 GGUFs would have been allocated with qnext-style
packed buffers (wrong shape, would segfault in build_graph).

What changes:
- src/llama-hparams.h: new field `bool use_qnext_state_layout = false`.
  Documented inline as the explicit discriminator between the two
  recurrent state packings (qnext GDN vs standard Mamba SSM).
- src/llama-hparams.cpp: n_embd_r() and n_embd_s() now branch on the
  new flag instead of `ssm_n_group > 0`.
  · Default branch (use_qnext_state_layout = false) is the unified
    Mamba formula taken verbatim from upstream llama.cpp:
       n_embd_r = (d_conv-1) * (d_inner + 2*n_group*d_state)
       n_embd_s = d_state * d_inner
    Mamba-1 stays byte-equivalent because n_group=0 collapses the
    formula back to (d_conv-1)*d_inner.
  · qnext branch (use_qnext_state_layout = true) is the existing
    GDN packed formula, untouched.
- src/llama-hparams.cpp: the three Qwen3.5 GDN cases in
  llm_load_hparams (LLM_ARCH_QWEN3NEXT, LLM_ARCH_QWEN35MOE,
  LLM_ARCH_QWEN35) explicitly set the flag to true. The Phase 2
  cases for LLM_ARCH_MAMBA2 and LLM_ARCH_NEMOTRON_H_MOE leave it
  at the default (false).

What does NOT change:
- The qnext logic itself (formula, scheduler, slot management) is
  byte-equivalent. Qwen3.5 production path is preserved.
- Mamba-1 formulae are preserved (collapsed n_group=0 case).
- llama-build-context.cpp still aborts at line 9983 for Nemotron-H
  because Phase 3.3 hasn't ported build_mamba2 / build_nemotron_h_moe
  yet — that's the next milestone.

Validation:
- Build with CUDA 12.8 sm_120 succeeds. Same single pre-existing warning.
- Qwen3.5 chimere-v3-ramp.gguf GPU smoke test (-ngl 99 --n-cpu-moe 3
  -fa on -ctk q8_0 -ctv q4_0 -c 8192):
  · "What is 2+2?" generates "<think>\n\n</think>\n\n2+2 equals 4."
  · 94.31 tok/s eval (B.1 baseline 94.10 tok/s, prod baseline 93 tok/s)
  · Zero regression
- Nemotron-Nano UD-IQ3_XXS CPU load test:
  · Phase 2 KV buffer was 6168.62 MiB; Phase 3.1.B.2 is 6190.00 MiB.
    The +21.38 MiB delta is the conv state being allocated for the
    first time — n_embd_r() now returns the correct non-zero size
    instead of falling into the qnext "everything in V slot" branch.
  · Failure point is still build-context.cpp:9983 (the GGML_ABORT
    fallback in build graph dispatch). This is the EXPECTED Phase 2
    end state — Phase 3.3 will add the build_mamba2 /
    build_nemotron_h_moe cases that consume these correctly-sized
    buffers.

Why a flag instead of arch dispatch:
- llama_hparams currently has no access to the LLM_ARCH enum (it's
  filled before arch is fully resolved on llama_model). Adding a
  bool flag is more local and follows the same pattern upstream uses
  for wkv_head_size (RWKV), n_shortconv_l_cache (LFM2), and
  n_embd_head_kda (Kimi KDA): each non-Mamba SSM family advertises
  its own layout via a dedicated field, and the unified Mamba
  formula is the fallthrough.
- Naming the flag `use_qnext_state_layout` rather than e.g.
  `ssm_layout_kind` keeps it boolean and explicit; if a third layout
  ever appears, it should get its own dedicated flag rather than
  sharing this one.

Phase 3 next steps (NOT in this commit):
- Phase 3.2: port ggml-cuda/ssm-scan.cu (chunked associative scan
  kernel for Mamba-2 / Nemotron-H, ~342 lines, CUB-based, needs
  sm_120 verification).
- Phase 3.3: port build_mamba2() and build_nemotron_h_moe() into
  llama-build-context.cpp, wire into the dispatch switch at
  line ~9982 to replace the GGML_ABORT.
- Phase 3.4: end-to-end load + generate Nemotron-Nano on GPU,
  bench tok/s, validate Qwen3.5 zero regression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3.2 replaces ik_llama's ggml_ssm_scan body and signature with the
upstream unified Mamba-1+Mamba-2 form (last arg renamed `sq` → `ids`,
shapes go from 2D to 4D). The only consumer in src/ is the existing
llm_build_context::build_mamba() at llama-build-context.cpp:6104, which
calls ggml_ssm_scan with the OLD shapes and would fail to compile after
Phase 3.2 lands.

Mamba-1 has never worked end-to-end in ik_llama: there is no CUDA scan
kernel, the CPU op was a partial stub, and no production GGUF (Qwen3.5,
chimere-v3-ramp, anything used by chimere-server / qwen35-custom.service)
calls this code path. Stubbing build_mamba() to abort at runtime is safe
and isolates the Phase 3.2 ggml change from the Phase 3.3 build-graph
rewrite.

What changes:
- src/llama-build-context.cpp: the original build_mamba() body is wrapped
  in `#if 0 ... #endif` (preserved verbatim for Phase 3.3 reference, just
  not compiled). A 7-line stub returning GGML_ABORT replaces it. The
  function signature is unchanged so the dispatch in llama_build_graph
  (line 9857) still links.

What does NOT change:
- The dispatch case `case LLM_ARCH_MAMBA: result = llm.build_mamba()`
  remains. If a Mamba-1 GGUF were ever loaded, it would now hit the
  GGML_ABORT with a clear message instead of doing wrong-shape ssm_scan
  calls.
- ggml/, src/llama-hparams.{h,cpp}, src/llama-arch.{h,cpp},
  src/llama-load-tensors.cpp, src/llama-model.cpp — all untouched.

Validation:
- Pure stub, no runtime path changed. Build verified after Phase 3.2
  lands (Phase 3.2 agent build will succeed against this stub, where it
  would have failed against the original body).

Phase 3.3 will rewrite build_mamba() on top of the new ggml_ssm_scan
(4D shapes, ids), and add build_mamba2() and build_nemotron_h_moe()
as siblings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace ik_llama's old Mamba-1-only ggml_ssm_scan with upstream's
unified Mamba-1 + Mamba-2 op, and add the CUB-based CUDA backend.
Mamba-1 vs Mamba-2 is discriminated at runtime via A->ne[0] (1 for
Mamba-2, d_state for Mamba-1). Shapes go from 2D/3D to 4D
(head_dim x n_head x n_seq_tokens x n_seqs for x).

What changes:
- ggml/include/ggml.h: ggml_ssm_scan signature updated to upstream
  unified Mamba-1+Mamba-2 form (8 args, last renamed sq -> ids).
  Added shape-convention comment.
- ggml/src/ggml.c: ggml_ssm_scan body + ggml_compute_forward_ssm_scan
  CPU impl ported from upstream llama.cpp/ggml-cpu/ops.cpp. Mamba-2
  branch uses GGML_SIMD/GGML_F32_VEC_* macros (already present in
  ik_llama). Mamba-1 branch is the scalar reference (d_state usually
  16, expf inside the loop, no SIMD as upstream).
- ggml/src/ggml-cuda/ssm-scan.cu: NEW, copied verbatim (342 lines)
  from upstream llama.cpp/ggml-cuda/ssm-scan.cu. CUB path active
  (CUDART_VERSION >= 11070), no sm_120 tweaks needed -- builds and
  links cleanly with --generate-code arch=compute_120,code=sm_120.
- ggml/src/ggml-cuda/ssm-scan.cuh: NEW, verbatim from upstream.
- ggml/src/ggml-cuda.cu: include ssm-scan.cuh; dispatch
  GGML_OP_SSM_SCAN -> ggml_cuda_op_ssm_scan; supports_op gating
  mirrors upstream (Mamba-2: d_state in {128,256} && d_head%16==0;
  Mamba-1: d_state==16 && d_head==1 && n_head%128==0 && n_group==1).
- ggml/src/CMakeLists.txt: explicit list(APPEND GGML_SOURCES_CUDA
  ggml-cuda/ssm-scan.cu) + REMOVE_DUPLICATES, so the new file is
  picked up even when cmake's GLOB cache is stale (ik_llama doesn't
  use CONFIGURE_DEPENDS).

What does NOT change:
- src/llama-build-context.cpp untouched (Phase 3.3 territory). Its
  one existing call to ggml_ssm_scan (legacy Mamba-1 build path)
  still compiles thanks to identical arg count/types, but its
  shape-prep code is now stale and the Mamba-1 build path remains
  broken until Phase 3.3 rewrites it for the 4D upstream layout.
- src/llama-hparams.{h,cpp} untouched (Phase 3.1 final state).
- src/llama-arch.{h,cpp}, src/llama-load-tensors.cpp,
  src/llama-model.cpp untouched (Phase 1+2 final state).
- src/llama-delta-net.cpp untouched. The Qwen3.5 production path
  uses GGML_OP_DELTA_NET, not GGML_OP_SSM_SCAN, so this change has
  zero impact on the Qwen3.5 forward pass.

Validation (no runtime test -- production GPU was busy with
qwen35-custom.service holding ~14.9 GB VRAM):
- cmake auto-reconfigured (CMakeLists.txt newer than cache) and
  built llama-cli + llama-server cleanly with CUDA 12.8 sm_120.
- nm libggml.so shows: T ggml_ssm_scan, t
  ggml_compute_forward_ssm_scan_f32.isra.0, T ggml_cuda_op_ssm_scan,
  W ssm_scan_f32<128,16,L> for L=0..8 (Mamba-1 fallback templates),
  W ssm_scan_f32_group<4,128> and <8,256> (Mamba-2 kernels).
- ssm-scan.cu.o is 374 KB, ELF x86-64 relocatable.

Next: Phase 3.3 -- in src/llama-build-context.cpp, port build_mamba2
and build_nemotron_h_moe from upstream llama-model.cpp. The existing
llm_build_mamba (around line 6199) needs its 4D shape preparation
rewritten: x must be reshaped to {head_dim, n_head, n_seq_tokens,
n_seqs}, dt to {n_head, n_seq_tokens, n_seqs}, A to {1, n_head} for
Mamba-2 or {d_state, n_head} for Mamba-1, and the sq tensor must be
replaced by an ids vector of length n_seqs (per-sequence source
state index). The new ggml_ssm_scan asserts will fire loudly at
graph build time if the shapes are wrong.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses the single YELLOW finding from the Phase 3.2 security audit
(agent ac852130). A malformed or malicious Mamba-2 GGUF with
`ssm_n_group = 0` would cause a division by zero in Phase 2's tensor
allocation helpers (`d_inner / n_group` in the ssm_norm shape
computation), and extremely oversized values of `ssm_d_inner` /
`ssm_d_state` could OOM the system during tensor allocation.

All real-world Mamba-2 GGUFs (including Nemotron-3-Nano-30B-A3B used
as the target for this backport, which has n_group=8 d_inner=4096
d_state=128) satisfy these bounds with huge margin — the asserts are
defensive against corruption or adversarial inputs, not for valid
production models.

What changes:
- src/llama-hparams.cpp: 6 GGML_ASSERT calls added to both the
  LLM_ARCH_MAMBA2 and LLM_ARCH_NEMOTRON_H_MOE cases in llm_load_hparams:
    * ssm_n_group > 0    (prevents divide-by-zero)
    * ssm_d_inner > 0
    * ssm_d_state > 0
    * ssm_d_inner % ssm_n_group == 0  (prevents division with remainder)
    * ssm_d_inner <= 65536   (sanity upper bound)
    * ssm_d_state <= 2048    (sanity upper bound)

What does NOT change:
- Nemotron-3-Nano-30B-A3B UD-IQ3_XXS loads unchanged: n_group=8,
  d_inner=4096, d_state=128 — all asserts pass.
- No Phase 3.1 / 3.2 behaviour modified.
- Qwen3.5 / Mamba-1 / every non-Mamba-2 arch unaffected (different
  switch cases, asserts not reached).

Validation:
- Build with CUDA 12.8 sm_120 succeeds (incremental, only
  llama-hparams.o rebuilt).
- Asserts have clear failure messages for debuggability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…on_h_moe

Implements the per-arch build graphs for Mamba-2 and Nemotron-H on top of
the Phase 3.2 ggml_ssm_scan op. Nemotron-3-Nano-30B-A3B-UD-IQ3_XXS now
runs end-to-end on CPU (CUDA_VISIBLE_DEVICES=) and generates tokens.

What changes
============

src/llama-build-context.cpp (~490 net new lines)

  - Anonymous-namespace helper `build_mamba2_layer(ctx0, gf, lctx, cb,
    cur, layer, ssm_ids, n_tokens, _, il)` ports upstream's
    `llm_build_mamba_base::build_mamba2_layer`. Differences vs upstream:
      * Single-sequence path (n_seqs=1, n_seq_tokens=n_tokens) — Phase 3.3
        does not yet support equal_seqs scheduling.
      * Reuses ik_llama's OLD `ggml_ssm_conv(s, x, c, sq)` (4-arg, Mamba-1
        era) instead of upstream's 2-arg variant — the upstream variant is
        not in ik_llama's ggml fork and porting it is out of Phase 3.3
        scope. The OLD op handles the channel-wide conv1d transparently
        when treating `d_inner + 2*n_group*d_state` as the channel count.
      * Uses the NEW Phase 3.2 `ggml_ssm_scan` (7-arg with 4D shapes and
        ids) for the SSM scan itself.
      * The recurrent state slot is always 0 in Phase 3.3 (no n_seq_max>1
        smoke test yet); kv_head parameter is documented as unused.

  - `llm_build_context::build_mamba()` rewritten on the new helper.
    Dispatches on `model.arch`: Mamba-2 hits `build_mamba2_layer`,
    Mamba-1 (LLM_ARCH_MAMBA) aborts cleanly with a clear message
    (Mamba-1 has no smoke target in ik_llama and the legacy `#if 0`
    block above the function is the textual reference if it ever needs
    one).

  - `llm_build_context::build_nemotron_h_moe()` — new method, ~120
    lines. Per-layer dispatch matches upstream nemotron-h.cpp:
      * `hparams.is_recurrent(il)` → SSM mixer via `build_mamba2_layer`
      * `hparams.n_ff(il) == 0` → attention layer (no RoPE), built
        manually with `llm_build_mul_mat_qkv` + `llm_build_kv`
      * else → dense FFN (LLM_FFN_RELU_SQR, LLM_FFN_PAR) or routed MoE
        with sigmoid gating + optional shared expert.

  - `llm_build_moe_ffn` extended with a gateless code path
    (`gate_exps == nullptr`): applies activation directly to up_exps,
    no gate*up combination. Required by Nemotron-H's gateless MoE.
    Other arches unaffected — they still pass non-null gate_exps.

  - Dispatch in `llama_build_graph` switch:
      * LLM_ARCH_MAMBA2 → `build_mamba()` (handles both Mamba-1 and -2)
      * LLM_ARCH_NEMOTRON_H_MOE → `build_nemotron_h_moe()`

src/llama-build-context.h
  - declare `build_nemotron_h_moe()`.

src/llama-context.h
  - new field `inp_ssm_ids` (I32 [n_seqs]) on llama_context — input
    tensor consumed by ggml_ssm_scan as the per-seq source state index.

src/llama.cpp
  - `llama_kv_cache_init` `qnext_recurrent` branch: for the standard
    Mamba layout (`!use_qnext_state_layout`, embd_r > 0) we now also
    allocate a 1D conv-state tensor in `cache.k_l[i]` of size
    `n_embd_r * qnext_state_slots`. The qnext GDN layout is unchanged
    (k_l/v_l stay null, all state in s_l).
  - `llama_set_inputs` now fills `inp_ssm_ids` with `kv_self.head + s`
    when set by the build graph.

Validation
==========

Build (CUDA 12.8 sm_120):
  ...
  [ 98%] Linking CXX executable ../../bin/llama-cli
  [ 98%] Linking CXX executable ../../bin/llama-server
  [100%] Built target llama-cli
  [100%] Built target llama-server

Qwen3.5 CPU non-regression smoke (chimere-v3-ramp.gguf, IQ3_S RAMP):
  > What is 2+2? Answer in one sentence.
  > <think>1.  **Identify the Question:** The user is asking "What is
  generation @ 15.41 tok/s, prompt eval @ 52.02 tok/s — coherent.

Nemotron-3-Nano-30B-A3B (UD-IQ3_XXS, 17 GB) CPU smoke:
  > Hi  → 1 token, immediate EOS, exit clean
  > The quick brown fox jumps over → "subt" + 9 more tokens, exit clean
  > The capital of France is → 8 tokens, exit clean
  generation @ 16.38 tok/s, prompt eval @ 41.03 tok/s
  Output is low-quality on CPU (likely IQ3_XXS quant + first-light
  smoke), but the SSM scan + Mamba-2 conv + Nemotron-H attention + MoE
  pipeline runs end-to-end without aborts.

Caveats / TODO for Phase 3.4
============================

  - n_seq_max > 1 / parallel sequences not supported (n_seqs hardcoded
    to 1 in build_mamba2_layer; the kv-cache slot is always 0).
  - Mamba-1 (LLM_ARCH_MAMBA) build path aborts at runtime — no
    smoke target in ik_llama, legacy `#if 0` body kept as reference.
  - read_kv_cache_data / write_kv_cache_data still assume the legacy
    K/V cache layout for the conv state (`n_embd_k_gqa(il) +
    n_embd_r()`). Save/restore of Nemotron-H state is broken until
    that path is updated. Smoke test does not exercise it.
  - GPU validation deferred to Phase 3.4. Phase 3.2 added the CUDA
    backend kernel for ggml_ssm_scan, but Phase 3.3 has only run on
    CPU (production qwen35-custom.service is the GPU resident).
  - Output quality of Nemotron-Nano IQ3_XXS on CPU is poor — could be
    from a) the aggressive UD-IQ3_XXS quant, b) a subtle correctness
    bug in the conv↔scan packing, or c) sampling issues with the
    Nemotron tokenizer. To diagnose post-Phase 3.4 with bf16/q8 GGUF.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous ggml_print_tensor in examples/eval-callback/eval-callback.cpp
had `sum +=` inside the truncated print loop. When ne[0] > 2*n (n=3 by
default), the inner loop would jump from i0=n to i0=ne[0]-n via
`i0 = ne[0] - n`, skipping the middle elements entirely. Result: `sum`
only included the first 3 + last 3 elements per row, NOT the full tensor.

Concretely, on Nemotron-3-Nano-30B-A3B input embedding {2688, 5}:
  - True full sum   = 1.460159 (13440 elements)
  - Old printer sum = 0.067017 (only 30 elements)

This mismatched the upstream llama.cpp eval-callback (common/debug.cpp)
which computes sum in a separate loop over ALL elements, making the two
binaries impossible to compare numerically when debugging arch backports.

This commit factors the per-element float read into a helper, computes
sum in its own loop over the full tensor (mirroring upstream), then
runs the truncated print as a separate pass. The print output is
unchanged; only the printed `sum =` value now reflects the real total.

What does NOT change:
- Production code paths (only examples/eval-callback affected).
- The print format (tensor structure, indices, value formatting).
- Any other binary or runtime behaviour.

Validation:
- Re-ran eval-callback on Nemotron-3-Nano-30B-A3B UD-IQ3_XXS with
  prompt "1+1=", confirmed inp_embd sum now matches upstream's
  byte-for-byte: 1.460159 (vs 0.067017 before this fix).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3.3 (build_mamba2_layer in llama-build-context.cpp) writes the new
recurrent state back to slot 0 unconditionally:

    const int64_t r_kv_head = 0;
    ...
    ggml_view_1d(ctx0, ssm_states_all,
                 d_state * d_inner * n_seqs,
                 r_kv_head * d_state * d_inner * ggml_element_size(ssm_states_all))

But the READ side (the `ids` input to ggml_ssm_scan, populated here in
llama_set_inputs) was sourcing from `kv_self.head + s`. For the qnext
GDN arches (LLM_ARCH_QWEN35*, LLM_ARCH_QWEN3NEXT) the recurrent cache
is genuinely flagged as `cache.recurrent = true` and `kv_self.head` is
maintained as the recurrent slot index, so that worked. But for the new
LLM_ARCH_MAMBA2 / LLM_ARCH_NEMOTRON_H_MOE arches the cache is
`cache.recurrent = false` (they are registered as "hybrid" in
llama-arch.cpp:255-263), and `kv_self.head` is the *attention* KV head
which advances by n_tokens after every batch (llama.cpp:3652).

After the prefill of "1+1=" (5 tokens), kv_self.head = 5. The next
ggml_ssm_scan call would then read `s0 = src0->data + 5*src0->nb[3]`
which is ~10 MB beyond the single-slot recurrent state cache — pure
out-of-bounds memory. On GPU this manifested as
`ggml_backend_cuda_synchronize: an illegal memory access`. On CPU it
silently produced corrupted recurrent state, garbage logits, and
sampler asserts.

Fix: with n_seq_max = 1 (the Phase 3.3 invariant), the recurrent state
cache has exactly one slot. The READ side now sources from slot
0..n_seqs-1, mirroring the WRITE side. This makes the conv-state and
SSM-state paths consistent (the conv-state uses inp_s_seq_qnext which
is also always zero-filled at lines 3110-3113 of this same file).

What does NOT change:
- The qnext GDN path is unaffected: it does NOT use inp_ssm_ids
  (only the new ggml_ssm_scan added in Phase 3.2 does).
- Multi-sequence (n_seq_max > 1) is still unsupported by Phase 3.3
  build_mamba2_layer; this fix preserves that limitation. When multi-seq
  lands, inp_ssm_ids will need a proper "recurrent slot index" path
  that does NOT alias kv_self.head.
- Production Qwen3.5 path: untouched, this code is gated on
  `lctx.inp_ssm_ids != nullptr` which is only set by build_mamba/
  build_nemotron_h_moe.

Validation:
- Re-ran eval-callback dump on Nemotron-3-Nano-30B-A3B UD-IQ3_XXS for
  prompt "1+1=". With this fix, all intermediate tensor sums match
  upstream llama.cpp eval-callback within numerical noise (0.04-0.1%)
  through the entire layer 0 mixer (ssm_in_proj, ssm_conv, silu,
  ssm_scan, mamba2_y_add_d, swiglu, ssm_norm, ssm_norm * weight).
- One residual divergence remains at MUL_MAT(ssm_out, y) → 4× sum
  mismatch. Documented in PHASE_3_DEBUG_FINDINGS.md, requires further
  investigation. Independent of this fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… FFNs)

Symptom: Nemotron-3-Nano-30B-A3B produced gibberish on Q4_0 AND on UD-IQ3_XXS
through the new Mamba-2 backport, with the divergence vs upstream llama.cpp
starting at the *first* MoE FFN layer (block 1, all subsequent blocks
compounding error). attn_norm-0 and attn_norm-1 matched upstream byte-for-byte;
attn_norm-2 diverged 23%, and from there error grew until the LM head.

Root cause: llm_build_ffn() unconditionally folded the parallel "gate" path
back in via `cur = ggml_mul(cur, tmp)` whenever `type_gate == LLM_FFN_PAR`,
even when no gate weight had been passed. For gateless FFNs (Nemotron-H shared
expert: relu²(up_proj) only), the earlier `} else { cur = tmp; }` already set
`cur` to `tmp`, so the unconditional mul collapsed the activation to
`relu²(up) * up` instead of `relu²(up)`. The same bug exists in the fused
fast path a few lines above (`ggml_fused_mul_unary(cur, tmp, ...)` for
SILU/RELU/GELU + LLM_FFN_PAR + no-gate would yield `op(up) * up`).

This codepath was unreachable until Nemotron-H, because every prior caller of
LLM_FFN_PAR also passed a real gate weight (SwiGLU style). The trace symptom
"ffn_gate_par-1" appearing in ik_llama but absent from upstream's identical
forward graph is what pinned down the regression.

Fix: gate both the fused fast path and the slow path on `gate != nullptr`. No
behavior change for any model that actually uses a gate.

Validation:
- llama-cli on Nemotron-3-Nano-30B-A3B Q4_0 (CPU): now answers
  'The capital of France is Paris.' instead of 'optimal opt A A Sole B'.
- llama-cli on the UD-IQ3_XXS GGUF (CPU): now answers
  'Paris, and the capital of Italy is Rome, but the capital of Ireland is Dublin.'
- This invalidates the earlier 'iqk IQ3_XXS kernel is broken' diagnosis from
  ~/Bureau/phase3-bench-2026-04-06/PHASE_3_PERMUTE_BUG.md — keeping that doc
  as a record of how a coincidence (iq3_xxs vs q6_K layers happening to drift
  by similar magnitudes from accumulated error) misled the first debug pass.
Two follow-up fixes after rebasing the Phase 3.x backport on top of current
upstream `main` (ikawrakow/ik_llama.cpp@67fc9c5):

1. `hparams.n_embd_head_k` and `n_embd_head_v` are now per-layer member
   functions, not fields, since the Gemma4 SWA work landed. Update the
   single use site in `build_nemotron_h_moe()` to call the new accessors
   with `il=0` (Nemotron-H attention layers all share the same head_dim
   and the model has no SWA, so any layer index is representative).

2. `llama_model::cache_size()` was updated upstream to read recurrent
   layers via `hparams.n_embd_v_s()` for Gemma4-style hybrids; the
   Phase 2 rename of `n_embd_k_s/v_s -> n_embd_r/s` did not catch this
   new caller because it was added by upstream after the rename was
   originally written. Switch the call to `hparams.n_embd_s()`.

No semantic change for either Qwen3.5 GDN (no SWA, recurrent path uses
n_embd_s = packed conv+ssm size as before) or Nemotron-H (no SWA, all
attention layers share head_dim). Build is green again on top of upstream
main; the rest of the Phase 3.x cherry-picks apply cleanly.
AIdevsmartdata added a commit to AIdevsmartdata/chimere that referenced this pull request Apr 6, 2026
Adds `bin/test-nemotron.rs` — a ~90-line standalone binary that proves
chimere-server's `LlamaForward` (the libllama FFI layer that backs
`GenericModel`) is fully wired against the new `ik_llama` Mamba-2 +
`nemotron_h_moe` backport (PR ikawrakow/ik_llama.cpp#1593).

What it does:
1. `chimere_deltanet::llama_backend::from_env()` — same entry point used
   by `GenericModel::from_env()`. No `Qwen35Model`, no `cudarc_shell`.
2. tokenize → forward_prefill → greedy argmax → forward_token loop →
   detokenize.
3. Prints generation rate + decoded text.

Validation on Nemotron-3-Nano-30B-A3B Q4_0 (sm_120 CUDA, NCMOE=30,
ctx=2048):

    prompt: "The capital of France is"
    prefill: 199 ms (30 tok/s prompt)
    gen:     45 tok/s, 333 ms / 15 tokens
    output:  "The capital of France is Paris.\" No extra text. Must not add
              anything else. Just that sentence"

Confirms end-to-end:
- Phase 3.3 graph builder (build_nemotron_h_moe) works on GPU
- Mamba-2 SSM scan CUDA kernel works
- Hybrid SSM + GQA + gateless RELU² MoE FFN all chained correctly
- chimere-server's tokenize/detokenize FFI bindings work for non-Qwen vocab

Remaining work (out of scope for this commit, tracked separately):
- bin/chimere-server.rs still hardcodes Qwen35Model. Wiring the
  arch-detection dispatch (Qwen35Model vs GenericModel) requires
  AppState polymorphism — Step 7 of the multi-arch refactor plan.
- CHIMERE_TOKENIZER is mandatory (no GGUF embedded fallback). For
  Nemotron, download tokenizer.json from `unsloth/Nemotron-3-Nano-30B-A3B`
  via huggingface-cli.

Usage:
    LD_LIBRARY_PATH=/path/to/ik_llama/build/{ggml/src,src} \
    CHIMERE_MODEL=/path/to/Nemotron-3-Nano-30B-A3B-Q4_0.gguf \
    CHIMERE_TOKENIZER=/path/to/Nemotron tokenizer.json \
    CHIMERE_NCMOE=30 CHIMERE_KV_MAX_SEQ=2048 \
    cargo run --release --bin test-nemotron

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AIdevsmartdata added a commit to AIdevsmartdata/chimere that referenced this pull request Apr 7, 2026
…F card draft

Phase C of the Step 7 / multi-arch dispatch milestone. Captures the
state of chimere as of 2026-04-07 evening, after the 10 atomic Step 7
commits and PR ikawrakow/ik_llama.cpp#1593 landed end-to-end.

## README.md (full rewrite, 199 -> ~570 lines)

- New 1-line pitch mentions both architecture families
- Highlights box: 8 bullets (multi-arch dispatch, Qwen3.5 prod stack,
  Mamba-2 / Nemotron-H via PR #1593, engram, sm_120, OpenAI-compat HTTP)
- Supported architectures table with arch / variant / GGUF metadata /
  status / perf / notes
- Quick start: 5 commands covering Qwen3.5 prod, Nemotron via swap,
  test-nemotron smoke, curl examples
- ASCII architecture diagram (HTTP -> AppState -> Mutex<AppStateModel>
  -> {Qwen35Model | GenericModel} -> libllama -> ik_llama -> CUDA sm_120)
- Features detailed table grounded against chimere-server source
- Full CHIMERE_* env var table from the source (not the README)
- Performance section with the 3 narrative corrections inline:
  * MTP "+49.5%" softened to "infrastructure present, gated, fix
    planned" with explicit reference to bench_mtp.rs:104-167 SKIPPED
  * Engram "+quality" reframed as "domain knowledge injection via
    Cuckoo n-gram logit bias", with explicit disclaimer about the
    GPT-2 wikitext-2 -13.39% being out of distribution
  * Nemotron 45 tok/s cited with full reproduction conditions
    (NCMOE=30, ctx 2048, sm_120 RTX 5060 Ti, test-nemotron binary)
- Backend section linking to PR #1593 + describing the Phase 3.x
  backport (Mamba-2 unified ggml_ssm_scan, nemotron_h_moe arch,
  FFN gateless+PAR fix, sm_120 CUDA SSM scan kernel)
- Models tested + broader Mamba-2 ecosystem hints (Granite 4.0,
  Falcon Mamba, Codestral Mamba, Bamba, Hymba — "should work, untested")
- Roadmap: Step 7.5 (FFI tokenizer fallback, multi-agent on Generic,
  streaming Generic), Mamba-3 backport (cite arXiv:2603.15569), NVFP4
- Citations: papers + repos + our PR

## CHANGELOG.md (NEW, Keep-a-Changelog format)

[0.2.0] - 2026-04-07 — the Step 7 release. Documents:
- Added: AppStateModel enum, GenericModel, test-nemotron, new env
  vars (CHIMERE_ENGRAM_DIR, CHIMERE_GENERIC_EOS, CHIMERE_FORCE_QWEN35)
- Changed: AppState::model field type, InferenceState::Generic
  variant, llm_build_ffn gateless+PAR guard
- Backend: ik_llama.cpp pinned to PR #1593 head
- Known limitations (Step 7 caveats: single-agent Generic, lossy
  streaming Generic, no engram on non-Qwen, n_seqs=1 hardcode)
- Validated runtime numbers
- Migration guide (CHIMERE_ENGRAM_DIR + Nemotron env vars)

## chimere-server/docs/STEP7_MULTI_ARCH.md (NEW, 426 lines)

Technical reference for the dispatch architecture:
- Why (Mamba-2 / Nemotron support requires non-Qwen35 code path)
- Approach (closed enum vs trait object — preserves Qwen35Model
  inherent methods)
- Diagram of the dispatch flow
- The Send-across-await constraint discovered during commit 7/10
  implementation: &Qwen35Model is !Send because Qwen35Model is
  !Sync (RefCells), so the typical "match &*model { Qwen35(qwen)
  => ... .await ... }" pattern breaks the axum Handler bound
- Fix pattern: resolve agent_id BEFORE locking the model, then
  pattern-match without any await inside the arms
- Tradeoffs table per Step 7 limitation with the Step 7.5 fix planned
- ChimereModel trait + variant inventory
- Code pointers (real line numbers from HEAD)

## chimere-server/docs/HF_MODEL_CARD_UPDATE.md (NEW, draft)

Draft sections for the Kevletesteur/Qwen3.5-35B-A3B-Chimere-v3-GGUF
HuggingFace model card, to be applied via `hf` CLI:
- Compatible runtimes section (NEW): chimere-server first, stock
  llama-server as fallback
- Quick start REPLACE: chimere-server example replaces stock
- Backend section (NEW): ik_llama.cpp fork + PR #1593 + chimere link
- Multi-architecture support section (NEW): mention Nemotron-H
  works on the same backend at ~45 tok/s
- 3 narrative corrections applied consistently with the README

The doc agent flagged 3 stale doc-comments in chimere_model.rs:28-37
and bin/chimere-server.rs:5-31 as "noticed but not fixed" — to
address in a follow-up cosmetic commit.

GH topics updated separately via gh api: 11 -> 18, added
mamba2, mamba, nemotron-h, hybrid-ssm, ffi, sm120, state-space-models.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AIdevsmartdata added a commit to AIdevsmartdata/ik_llama.cpp that referenced this pull request Apr 7, 2026
The fork's main branch tracks upstream `ikawrakow/ik_llama.cpp` and
inherits its README verbatim. Add a top-level `FORK_NOTES.md` so
visitors landing on github.com/AIdevsmartdata/ik_llama.cpp can tell
at a glance:

- this is a fork (not the upstream)
- what was added (12-commit Phase 3.x backport: Mamba-2 + Nemotron-H
  MoE arch support, with all the ik_llama perf optimizations preserved)
- where to find the clean PR-ready branch (mamba2-nemotron-h-backport)
- the upstream PR being offered (ikawrakow#1593)
- end-to-end validation results (Nemotron-3-Nano-30B-A3B Q4_0 +
  UD-IQ3_XXS, ~45 tok/s on RTX 5060 Ti sm_120)
- caveats / scope (n_seqs=1, state save/restore not adapted, Mamba-1
  stubbed, OLD ggml_ssm_conv reused)
- downstream consumer (chimere-server)
- build instructions for sm_120 (Blackwell consumer)
- sync policy
- contact path

Single doc-only commit on top of upstream main (67fc9c5). Does not
touch any source file. Safe to delete if upstream merges PR ikawrakow#1593.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ikawrakow

Copy link
Copy Markdown
Owner

Thank you for the PR.

This project does not have a formal AI policy (yet), but that does not mean that AI generated PRs are encouraged or appreciated. There have been very few AI generated pieces accepted so far. They have been either very limited in scope, where it has been easy to spot the bugs, or they have been rewritten (the Qwen-3.5 architecture started from an AI-generated PR before I rewrote most of it).

This PR adds a lot of new code, so my questions to you are

  • How well do you understand this code?
  • Has there been any real testing beyond AI generated slop?
  • Without looking at the code, it is not immediately clear which backend(s) are supported. Have they been tested?
  • Where are the performance figures?

And now a question to everybody else: is anyone actually using this model? I mean, we all know that everybody is curious to try when a new model comes out, but based on a quick look at the download counts on HF, Nemotron3-Nano has been download an order of magnitude less than Qwen-3.5-35B-A3B, for instance.

@hardWorker254

Copy link
Copy Markdown

Thank you for the PR.

This project does not have a formal AI policy (yet), but that does not mean that AI generated PRs are encouraged or appreciated. There have been very few AI generated pieces accepted so far. They have been either very limited in scope, where it has been easy to spot the bugs, or they have been rewritten (the Qwen-3.5 architecture started from an AI-generated PR before I rewrote most of it).

This PR adds a lot of new code, so my questions to you are

  • How well do you understand this code?
  • Has there been any real testing beyond AI generated slop?
  • Without looking at the code, it is not immediately clear which backend(s) are supported. Have they been tested?
  • Where are the performance figures?

And now a question to everybody else: is anyone actually using this model? I mean, we all know that everybody is curious to try when a new model comes out, but based on a quick look at the download counts on HF, Nemotron3-Nano has been download an order of magnitude less than Qwen-3.5-35B-A3B, for instance.

I personally don't use Nvidia AI models at all. There are several reasons for this:

  1. Size. Even the Q2_K_L quant is larger than 16 gigabytes (Nemotron-3-Nano-30B-A3B), which excludes its use on systems with small resources.
  2. I don't think Nvidia has trained its models on datasets that have Russian, which is important enough for me to support.

Nemotron-Cascade-2 looks attractive, but I don't think its size is worth it. Gemma4 and Qwen3.5 look more interesting.

@AIdevsmartdata

AIdevsmartdata commented Apr 7, 2026 via email

Copy link
Copy Markdown
Author

@AIdevsmartdata

Copy link
Copy Markdown
Author

Draft reply to ikawrakow on PR #1593

This is the draft. Read it, edit anything you want, then I'll post it via gh pr comment ikawrakow/ik_llama.cpp#1593 --body-file ....


Hi Iwan, thank you for reading the PR and for the honest review — it's exactly the kind of feedback I was hoping for.

I want to be upfront: yes, this PR was written with AI assistance (Anthropic Claude Opus 4.6, used as a pair-programmer over a few intensive debugging sessions). I'll answer your four questions directly, then I'll explain why I opened the PR knowing the bar for AI-assisted work in this project, and what I plan to do next.

1. How well do I understand the code

Honestly: well enough to have hit the bugs, debugged them with comparative eval-callback traces against ggml-org/llama.cpp upstream on the same GGUF, and committed the fixes myself. Specifically:

  • Phase 3.3 commit ce543ad (build_mamba2_layer): I read upstream's mamba-base.cpp::build_mamba2_layer line by line and mirrored it into the ik_llama context-builder pattern. The 4-arg ggml_ssm_conv reuse for the conv1d step (instead of porting upstream's new 2-arg concat(state, transpose(xBC)) → ggml_ssm_conv(conv_x, weight) rewrite) is an explicit phase-3 hack that I documented in the source comments — it's numerically identical for n_seqs=1 but adds a few graph nodes per layer. Reworkable.

  • Commit 9bc3ee1 (inp_ssm_ids fix): the bug was that kv_self.head is an attention KV-head index that advances += n_tokens per decode (src/llama.cpp:3652), while the recurrent state cache for hybrid Nemotron-H has exactly one slot. Reading at kv_self.head * src0->nb[3] walked out of bounds after the very first prefill. I caught it by instrumenting inp_ssm_ids and checking the offsets.

  • Commit 38cdc3b (llm_build_ffn gateless+PAR fix): this is the one that ate most of the debug session and I'm most confident about. llm_build_ffn was unconditionally folding cur = ggml_mul(cur, tmp) for LLM_FFN_PAR, even when no gate weight was passed. For Nemotron-H's gateless relu²(up_proj) shared expert, this collapsed the activation to relu²(up) * up instead of relu²(up). Every prior caller of LLM_FFN_PAR happened to also pass a real gate (SwiGLU), so the gateless code path was unreachable until Nemotron-H exercised it. I found the bug by comparing tensor sums layer-by-layer between this fork and upstream llama.cpp: attn_norm-0 and attn_norm-1 matched byte-for-byte, attn_norm-2 started diverging, and the trace showed an extra ffn_gate_par-1 op present in this fork but absent from upstream — the smoking gun. Fix is two gate && guards.

  • Commit ee0c424 (eval-callback printer fix): minor but real. The old printer summed only the displayed head/tail elements of each tensor, which made comparing two binaries on the same tensor impossible during debugging — every divergence was a coincidence on partial sums. The fix moves the sum to a separate full-tensor pass before the truncated print.

What I am NOT pretending to fully understand: the qnext / GDN cache layout decisions you made for Qwen-3.5, the multi-slot attention KV scheduler, and the deeper iqk matmul template metaprogramming. I deliberately did not touch any of those — Phase 1 added new arch tags, Phase 2 added new tensor allocators, Phase 3 added a new graph builder next to the existing one. The qnext path is gated behind a use_qnext_state_layout flag and is unchanged for Qwen-3.5.

2. Real testing

End-to-end on Nemotron-3-Nano-30B-A3B (unsloth/Nemotron-3-Nano-30B-A3B-GGUF):

GGUF Backend Output
Q4_0 (uniform) sm_120 CUDA What is the capital of France? → "The capital of France is Paris." Provide exactly that line, no extra text.
Q4_0 CPU only (-ngl 0) same coherent output
UD-IQ3_XXS (Unsloth dynamic, mixed iq3_xxs/q6_K) CPU only Paris, and the capital of Italy is Rome, but the capital of Ireland is Dublin.

For Qwen-3.5-35B-A3B Chimere v3 RAMP (the model I run in production every day, qnext path), I ran a 4-prompt regression test against a baseline captured before the PR — 4/4 PASS, no regression. The Qwen-3.5 hot path is byte-for-byte unchanged in this PR by construction; I was paranoid enough to verify it anyway because that model is what I actually use.

Beyond that there has been no formal MMLU / perplexity / multi-language sweep — I'd want to do that before the PR is mergeable, and the bar should be "matches upstream llama.cpp on the same GGUF within numerical noise", which I have not yet established at scale.

3. Backend coverage (and which were tested)

  • CPU: validated end-to-end on both Nemotron Q4_0 and UD-IQ3_XXS via llama-cli -ngl 0. Output coherent.
  • CUDA sm_120 (Blackwell consumer, RTX 5060 Ti): validated end-to-end via my downstream chimere-server HTTP wrapper, ~45 tok/s gen on Nemotron Q4_0 with -ngl 99 --n-cpu-moe 30 -c 2048 -ctk q8_0 -ctv q4_0. The new ggml/src/ggml-cuda/ssm-scan.cu kernel was ported from upstream ggml-org/llama.cpp/ggml/src/ggml-cuda/ssm-scan.cu and exercises the d_state=128 / head_dim=64 path that Nemotron uses.
  • CUDA sm_89 / sm_80: not tested locally — I only have the 5060 Ti. Should work because the new kernel is the same one upstream ships, but I cannot claim it.
  • Metal / Vulkan / SYCL / HIP: NOT tested. I do not have the hardware. The CUDA kernel is the only backend port in the PR; those backends would presumably need their own kernels added by someone with the hardware.

This is a real gap in the PR and you're right to point it out. If the project gates merges on multi-backend coverage, this PR cannot land as-is — at minimum it needs Metal / Vulkan equivalents from people who can test them.

4. Performance figures

On RTX 5060 Ti (sm_120, Blackwell consumer), CUDA 12.8, build flags -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=120 -DGGML_NATIVE=OFF:

New Mamba-2 / Nemotron-H path (this PR)

Model Quant NCMOE Ctx Gen tok/s Notes
Nemotron-3-Nano-30B-A3B Q4_0 30 2048 ~45 new path validated end-to-end
Nemotron-3-Nano-30B-A3B UD-IQ3_XXS 30 2048 similar also validated, agentic-ready quant

Qwen-3.5-35B-A3B (qnext path, untouched by this PR — included as non-regression sanity check)

Quant Runtime NCMOE / NGL Slots Ctx Gen tok/s Prefill tok/s VRAM free
IQ3_S custom-mix ("masterpiece", 15 GB) ik_llama sm120 ncmoe 2 1 10K 98.6 1357 641 MB
IQ3_S custom-mix ik_llama sm120 ncmoe 0 3 196K (3×64K) 91 184 ~500 MB
IQ3_S (Unsloth UD) stock b8125 ncmoe 0 3 196K 69 ~150 ~500 MB
Q4_K_M (21 GB) ik_llama sm120 ncmoe 14 1 64K 62.3 314 348 MB
Q4_K_M stock b8125 ncmoe 14 1 16K 52 104 679 MB
Q5_K_XL (25 GB) ik_llama sm120 ncmoe 18 1 64K 49.9 257 454 MB
Q5_K_XL stock b8125 ncmoe 20 1 64K 42 97 ~1.2 GB

ik_llama vs stock b8125 head-to-head deltas on the same GGUF / same hardware (these are part of why I picked your fork as the backend in the first place):

Metric Model / quant ik_llama stock Δ
Gen tok/s Qwen-3.5-35B-A3B Q4_K_M 61.4 52.0 +18%
Prefill tok/s Qwen-3.5-35B-A3B Q4_K_M 187 104 +80%
Gen tok/s Qwen-3.5-35B-A3B IQ3_S 35B MoE 91 69 +32%
Gen tok/s Qwen-3.5-35B-A3B Q5_K_XL ncmoe18 vs ncmoe20 49.9 42 +19%
Prefill tok/s Q5_K_XL 257 97 +165%
Gen tok/s Qwen-3.5-9B Q8_0 dense 44.4 41.1 +8%
Prefill tok/s Qwen-3.5-9B Q8_0 dense 3370 2815 +20%
Gen tok/s Qwen-3.5-27B Opus-distilled IQ4_XS dense 25.65 23.41 +9.6%
Prefill tok/s same 1067 962 +11%

The dense gains are modest (+8-10 % gen) but the MoE gains (+18 % to +32 % gen, +80 % to +165 % prefill) are exactly the reason ik_llama is the right backend for the desktop-MoE niche. This is also the reason the Mamba-2 backport is worth doing in your fork specifically rather than just using stock — once the new arch lands, hybrid SSM-MoE models like Granite-4.0 H-Tiny, Falcon-H1, Bamba, Hymba, eventually Jamba inherit the same +18-32 % MoE advantage.

The full table with exact ncmoe / ctx / VRAM figures across all quants is at https://github.com/AIdevsmartdata/chimere/blob/main/benchmarks/benchmark-qwen35-2026-03-07.md.

Hardware bottleneck — why we target 15-17 GB models

The RTX 5060 Ti is a consumer Blackwell with PCIe 5.0 ×8 (it physically has 16 lanes wired but the Ti SKU is x8-electrical). For MoE models with --n-cpu-moe > 0, every CPU-offloaded expert routes through that x8 link on every token, and we measured the link as the dominant bottleneck above ncmoe ≈ 14 — past that point the gains from offloading more experts are eaten by PCIe transfer time. The practical consequence is that on this class of hardware the sweet spot is a 15-17 GB GGUF that fits entirely on the GPU with zero CPU offload (or, at most, ncmoe 2-4 to bleed off the very largest experts). That is exactly why my production GGUF is a custom mixed-precision RAMP recipe at 3.78 BPW, 15.2 GB, designed to land just under the 16 GB cliff with no PCIe round-trip per token. It's also why an oversized hybrid SSM model like Nemotron-3-Nano-30B-A3B (Q4_0 ≈ 17 GB, UD-IQ3_XXS ≈ 16.9 GB, Q2_K_L > 16 GB even at 2 bits, as you noted) is structurally a poor fit for this niche, and why Qwen-3.5 / Gemma-4 in the 27B-class are the right targets going forward.

I have not yet run a head-to-head Nemotron benchmark of this PR vs stock ggml-org/llama.cpp on the same GGUF / same hardware. That's the next thing I'd add if there's interest in landing the PR — the interesting metric is whether the +18-32 % MoE advantage above also applies to nemotron_h_moe (the dispatch goes through the same iqk_mul_mat path, so it should, but I haven't proven it).


Quality benchmarks and what actually moved the needle on this hardware

Beyond raw throughput, here are the quality numbers we have on the production stack — and, crucially, which optimizations actually paid off for us on consumer hardware versus which were null results. The honest delta between MTP / DFlash (negative or zero) and Engram logit-bias (clear positive) is part of why I want this PR landed: it shifts the focus from speculative-decoding tricks (which break on hybrid SSM-attention models because of the GDN state-rollback barrier) to what does work on a single 16 GB consumer GPU.

Qwen-3.5-35B-A3B Chimere v3 RAMP (Kevletesteur/Qwen3.5-35B-A3B-Chimere-v3-GGUF, 1565 downloads)

Benchmark Score Source
IFEval (15 instruction tests) 100 % (15/15) hand harness
GSM8K CoT 8-shot (1319 questions) 84.0 % flexible / 83.2 % strict lm_eval gsm8k_cot 8shot v3
HumanEval (30 problems, executed) 83 % (25/30) custom agent harness
BFCL tool-calling (20 questions) 75 % custom agent harness
GPQA-diamond zero-shot 47.98 % lm_eval
ARC-C 59.3 % / 62.2 % norm lm_eval
Adversarial edge cases (15 tests) 100 % prompt-injection resistance
Speed (chimere-server HTTP path) ~80 tok/s gen, 789 tok/s prefill RTX 5060 Ti, NCMOE=3, ctx 64K

The full v3 model card with comparison to v1 (Kevletesteur/Qwen3.5-35B-A3B-Chimere-Distilled-GGUF, 1591 downloads) and the BF16 source is at https://huggingface.co/Kevletesteur/Qwen3.5-35B-A3B-Chimere-v3-GGUF. The v1 → v3 delta is: IFEval 67 % → 100 %, GSM8K 52 % → 84 %, HumanEval 97 % → 83 % (regression on code, intentional tradeoff for the IFEval+GSM8K gain), BFCL 90 % → 75 % (same tradeoff). v1 stays the right choice for code-heavy use; v3 is the new general-purpose default.

Engram (n-gram logit bias) — the optimization that actually worked for us

I run an n-gram logit-bias overlay on top of the C++ fast sampler (Cuckoo filter, 4 prebuilt domain tables: kine 20 MB, code, cyber, general — ~21 MB total). The interesting bit is the production ablation on our internal ODO 10-task benchmark:

Engram config ODO bench (10 tasks)
v1 α = 0.35, applied during <think> blocks and response 77 % ← previous nightly default
OFF α = 0 (baseline) 85 %
v2 α = 0.1, response-only, NEST adaptive per-token 88 % ← current production

Net: +11 pp over no-engram, +3 pp over the OFF baseline, on a benchmark suite where the baseline was already passing. The root cause of the v1 regression was bias-during-thinking and α too high; v2 fixed both with tighter, response-phase-only application. This is small in absolute terms but it's the only inference-time optimization that gave us a positive measurable delta on this hardware.

What did NOT pay off (negative results we logged)

Two optimizations we instrumented and benchmarked at length on the same hardware, both with negative net result, both because of constraints specific to consumer-class GPUs:

Optimization Acceptance / quality Wall-clock Verdict
MTP (multi-token prediction) for Qwen-3.5-35B-A3B MoE 84.8 % token acceptance rate 0.51× speedup (i.e. 2× slower) Negative. The MTP head is itself MoE (256 experts on CPU), so each draft costs as much as a main forward — speculative decoding can't win against itself.
DFlash (block diffusion drafter) τ = 6.06 (comparable to the original DFlash paper's τ=6.4 on holdout) 0.73× wall-clock (i.e. 27 % slower) Negative. The target model is too fast (93 tok/s on bare ik_llama) for speculative decoding to amortize the drafter cost. Independently, the GDN State Barrier makes draft rollback structurally impossible on hybrid SSM-attention models — recurrent state cannot be re-wound after a draft rejection.

We ran 8 DFlash architectures over 27 days. The DFlash result is reported in detail in our paper draft (link in the chimere-system landing card) and the negative wall-clock is the headline finding — speculative decoding is not the right lever on this class of hardware for this class of model. What works is reducing per-token compute and avoiding PCIe round-trips, not predicting more tokens per step. Engram + custom mixed-precision quantization (RAMP) + a tight 15-17 GB GGUF that fits with zero offload + the ik_llama iqk MoE matmul deltas above — that's the lever set that actually moves the needle.

The combination of (a) the 15-17 GB sweet spot, (b) the ik_llama iqk MoE perf, (c) Engram on Qwen-style vocabularies, and (d) the upcoming Mamba-3 work, is what makes this Mamba-2 PR a foundation rather than a one-off Nemotron port for me. The PR on its own doesn't close the loop; it unblocks the model architecture for the next quant + fine-tune + Mamba-3 cycle.


On your meta question — is anyone using Nemotron-3-Nano?

You're right that the HF download counts are an order of magnitude lower than Qwen-3.5-35B-A3B. I wouldn't argue otherwise. To be blunt: Nemotron-3-Nano was a proof-of-concept for me, not the destination. I picked it for the backport because (a) it has both Mamba-2 mixers AND a hybrid attention-MoE structure, so getting it to work end-to-end exercises basically every code path I needed for the real target, and (b) it has a public reference implementation in ggml-org/llama.cpp so I had a working oracle to debug against.

I share your assessment of the model itself: it's larger than necessary for what it does, and on a 16 GB consumer GPU it's at the edge of the envelope. I personally use Qwen-3.5-35B-A3B in production every day (the Chimere v3 RAMP variant on my server), not Nemotron. If I'm honest the Nemotron support is fungible — what I actually wanted was the underlying ggml_ssm_scan op + the build_mamba2_layer helper, because that's what unblocks the next generation of models I want to run on my desktop.

What I'm actually trying to build, and where this PR fits

I'm working towards a frontier-class single-desktop pipeline targeting consumer Blackwell (RTX 5060 Ti class, 16 GB VRAM). The end-to-end stack I have in flight:

  1. Base model: Qwen-3.5-27B dense (15 GB Q4_K_S, already running on the same hardware, ~23 tok/s on the nightly pipeline). I share your view that Gemma-4 and Qwen-3.5 are the right targets for desktop-class hardware. Nemotron and Qwen-3.5-35B-A3B are interesting upper bounds, but the 27B dense is the sweet spot.

  2. Reasoning + tool-calling fine-tune: Dr.GRPO agentic rewards on top of an SFT BF16 LoRA (xLAM-2 + Nemotron-Agentic + Opus traces). Target ~$40-50 on rented GPU for the full pipeline.

  3. OPSDC distillation (reasoning compression): -64% tokens on the chain-of-thought without losing accuracy. I have a working pipeline and a small dataset on disk.

  4. TurboQuant KV cache (arXiv:2504.19874, ICLR 2026): you already shipped k_cache_hadamard in this very project, which is the rotation step. I plan to enable it on the 27B nightly with Q4_0 keys, generate the d=256 codebook for Qwen-3.5-27B's head_dim=256 (currently missing), and follow upstream's TBQ3_0 / TBQ4_0 work in PR #21089. This would let me push context from 8K to 16-32K with the freed VRAM.

  5. Mamba-3 upcycle (this is the part where this current PR matters most): the Mamba-3 paper landed on arXiv on 2026-03-16 (arXiv:2603.15569, accepted at ICLR 2026, by the same Tri Dao + Albert Gu lineage). It's three concrete changes on top of Mamba-2: exponential-trapezoidal discretization (which lets the implicit convolution replace the explicit causal conv1d — i.e. removes the very Phase-3-hack ggml_ssm_conv reuse this PR ships), complex-valued state (data-dependent rotary equivalent, unblocks state-tracking benchmarks Mamba-2 cannot solve), and MIMO state update (matmul-based, +1.8 pts at 1.5B, half the latency at fixed quality). My Phase 3.x work here is deliberately the foundation for that — about 90 % of what I added (arch dispatch, cache layout, hparams plumbing, build_xxx_layer pattern) carries over to Mamba-3 directly; only the scan kernel changes.

  6. MoE upcycling of the 27B dense into a small-MoE (à la upcycling literature) so the same 16 GB budget can host more total parameters with sub-active compute.

  7. Self-improving loop: this is the part of the system that I've already shipped and that I think is genuinely novel. My downstream Rust HTTP server (chimere-server) carries an n-gram logit-bias overlay ("Engram"), 4 prebuilt tables (kine 20 MB, code, cyber, general) loaded via Cuckoo filter into the C++ fast sampler. There's a nightly read/write loop (engram-write-nightly.timer) that ingests scored production traffic, decays unused n-grams, and re-publishes the table. An orchestrator ("ODO", port 8084) sits in front and routes between intent classification, RAG, web search, dynamic engram, multi-agent pipelines and the model itself. The 27B nightly teacher pipeline runs at 00:30 every night on the same hardware, pre-empting the production server, training/scoring, then handing the GPU back. The whole point of having a fast Mamba-2 / Mamba-3 backend on consumer hardware is so this loop can keep running without renting cloud GPUs.

  8. Four-benchmark frontier target: I'm aiming for a single 27B-class model on a single 16 GB consumer GPU that posts competitive numbers on τ²-bench (agentic tool calling), BFCL-V4 (function calling), GSM8K / GPQA (reasoning) and LiveCodeBench v6 (code). Baseline today on the unmodified Qwen-3.5-27B: τ² ≈ 79 %, BFCL-V4 ≈ 68.5 %. Target after the SFT + Dr.GRPO + OPSDC + TurboQuant + (eventually) Mamba-3 upcycle: τ² in the 82-87 % range with the model fitting in 15 GB.

So in plain terms: this PR is the Mamba-2 foundation under the Mamba-3 + 27B-frontier work I'm actually trying to do. If you'd rather I close it and re-submit in two months as a smaller, more focused, multi-backend, benchmark-backed PR after the Mamba-3 work is done, I will. That might honestly be the right call for the project — by then the conv1d hack would be gone (replaced by Mamba-3's implicit convolution), the Metal / Vulkan kernels could be upstreamed by people who have the hardware, and there would be a real benchmark sweep behind it.

Or, if you'd prefer a different scope (just LLM_ARCH_MAMBA2, no Nemotron-H, no FFN gateless fix, no eval-callback printer fix; or only the FFN fix as a standalone bug-fix PR), I'm happy to split this one into smaller pieces. The 12-commit history was deliberately structured to be walkable so each step can be reviewed or dropped independently.

Whatever you decide, thank you for taking the time to look. The honest review is more valuable than a silent merge would have been.

Best,
Kévin Rémondière (@AIdevsmartdata) — and yes, Claude Opus 4.6 helped me write this reply too.


@FNsi

FNsi commented Apr 7, 2026

Copy link
Copy Markdown

I use Qwen3.5 for vision but indeed I prefer Qwen coder next for other stuff;(little tricky thing I found out is that seems those recurrent hybrids can reuse cache between each other via checkpoints or something else I don't know now thus...)

so basically I won't use those 30b moe in any case...

I think that impl is too complicated for a single model support and hard to maintain the consequence, around 400~600 lines change maybe be something more manageable... just a users opinion.

@ikawrakow

Copy link
Copy Markdown
Owner

I had a quick look at the change, it looks mostly OK.

Still, nobody seems excited.

From the plans it looks like the hope is that Mamba-3 will get used in new frontier models, and for that it would be useful to have this change set. This seems like the strongest argument for accepting the PR so far (but one could of course also just wait-and-see, and when new Mamba-3 based frontier models start coming out, add the changes then).

@giftick

giftick commented Apr 30, 2026

Copy link
Copy Markdown

I'd like to run Nemotron-Cascade-2-30B-A3B and it's future successors on desktop (linux x86_64 with an RTX 5060 Ti 16GB) because of it's math + statistics + reasoning strength that AFAICT exceeds that of Qwen3.6-35B-A3B and Qwen3-Coder-Next-80B-A3B, while also being smaller and hopefully faster when getting away with lower --n-cpu-moe.

Gemma-4-26B-A4B was rather disappointing for my uses; Qwen3.6-35B-A3B can be annoyingly verbose when thinking; and the dense models in the 30B range (Qwen3.6-27B, Gemma-4-31B) are much slower and more power hungry than their A3B MoE siblings.

Many thanks for the ik_llama project and the everyday value of it's efficiency !

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants