Skip to content

Mamba-3 (real algorithm) kernel-level PoC — Stages A–C complete - #136

Closed
jamesburton wants to merge 345 commits into
kkokosa:mainfrom
jamesburton:feature/mamba-3
Closed

Mamba-3 (real algorithm) kernel-level PoC — Stages A–C complete#136
jamesburton wants to merge 345 commits into
kkokosa:mainfrom
jamesburton:feature/mamba-3

Conversation

@jamesburton

Copy link
Copy Markdown

Summary

Implements the real Mamba-3 algorithm (Lahoti et al., arXiv 2603.15569, ICLR 2026) as a composable set of CPU kernels plus an end-to-end Mamba3Block primitive. Draft because PR #135 (feature/nemotron-and-mamba-2, Nemotron-H / Mamba-2 hybrid) should merge first — this branch stacks on top.

Why this is separate from PR #135

PR #135 was originally the feature/mamba-3 branch but turned out to implement the Mamba-2 hybrid used by NVIDIA Nemotron-3 (the "3" is the Nemotron version, not the Mamba generation). The real Mamba-3 — complex-valued state via data-RoPE on B/C, trapezoidal discretization, MIMO formulation — is a genuinely different algorithm published only March 2026. This branch implements that.

What lands here (26 tests, all green)

Stage A — 5 kernels + 22 unit tests

  • Mamba3Discretize — α/β/γ trapezoidal coefficients
  • Mamba3QkNorm — RMSNorm wrapper for B/C
  • Mamba3DataRoPE — data-dependent 2D rotation on B/C
  • Mamba3MimoProject — rank-R expand/contract (MIMO variant)
  • Mamba3SelectiveScan — trapezoidal scan (Eq. 9) with prev_Bx second state

Stage B — PyTorch reference fixtures + 3 per-kernel comparators

  • tests/DotLLM.Tests.Integration/Fixtures/Mamba3/capture_fixtures.py — runs a tiny Mamba-3 layer (d_model=8, nheads=2, headdim=4, dState=4, seqlen=4, SISO) through VikramKarLex/mamba3-minimal (Albert Gu-endorsed pure-PyTorch reference) and captures 24 intermediate tensors + all weights → fixture.json (18 KB).
  • verify_algorithm.py — pure-Python stdlib reimplementation of our kernels' math, runs in 100 ms without torch, confirms algorithm correctness independent of C# runtime.
  • 3 C# [SkippableFact] tests that load the fixture and verify our kernels against the reference within AbsTol=1e-6, RelTol=1e-5.

Stage C — Mamba3Block composer + end-to-end comparator

  • Mamba3Block.Forward(u, weights…, y, state, prev_Bx, …) chains: in_proj GEMM → 7-way split → softplus/sigmoid → Discretize → QkNorm → BC-bias broadcast → DataRoPE → SelectiveScan → D skip → silu(z) gate → out_proj GEMM.
  • Block_MatchesReference — feeds captured u through our block using captured weights, verifies y_final matches PyTorch reference element-wise (plus SSM state and prev_Bx for decode continuity).

What's deferred

Stage D — model loading

Blocked on upstream. No Mamba-3 checkpoints exist on HuggingFace, no llama.cpp LLM_ARCH_MAMBA3 support, no GGUF converter tensor mapping. Once a checkpoint lands, a follow-up PR adds Architecture.Mamba3, Mamba3TransformerModel, and ModelLoader.LoadFromGguf dispatch (analogous to the Nemotron-H work in #135).

MIMO-in-Block

The Mamba3MimoProject kernel exists and is unit-tested, but Mamba3Block.Forward is SISO-only. Adding the MIMO path (rank-R B/C reshape, mimo_x_proj/mimo_z_proj/mimo_down weights, post-scan rank contraction) is the next incremental deliverable — kernel-level, no upstream dependency.

Key findings / design decisions

  • λ is per-token, not per-head. Initial brief-to-agent was wrong; caught by fixture comparator when α matched but β didn't. Fixed in ef2cc35.
  • θ is per-token, not a learned per-head table. Matches the reference's input-projection split.
  • BC-bias is added after QkNorm, before RoPE, with per-head broadcast.
  • Two-SSD decomposition from the paper is inlined as a single pass in our scalar scan — equivalent result, half the memory traffic.

Test plan

  • 22 unit tests (kernel correctness vs hand-computed references)
  • 3 per-kernel fixture comparators (vs PyTorch reference)
  • 1 block-level end-to-end comparator (vs PyTorch reference)
  • Pure-Python algorithm verifier (max_abs 6.2e-7, max_rel 4.0e-7 against the reference)
  • MIMO path in Block (follow-up)
  • Stage D model loading (blocked on upstream)

🤖 Generated with Claude Code

@jamesburton

Copy link
Copy Markdown
Author

Lateral unlock (3cc6d38): safetensors loader for dense transformers. ModelLoader.LoadFromSafetensors(path) opens a HuggingFace-convention checkpoint (model.safetensors + config.json in the same directory), dispatches on architecture, and returns a loaded IModel. HfConfigExtractor mirrors GgufModelConfigExtractor over HF JSON fields (hidden_size, num_hidden_layers, num_key_value_heads, rope_theta, tie_word_embeddings, sliding_window, architectures[0] / model_type); TransformerModel.LoadFromSafetensors wires HF tensor names (model.layers.{i}.self_attn.{q,k,v,o}_proj, model.layers.{i}.mlp.{gate,up,down}_proj, model.{embed_tokens,norm}, lm_head), handles tie_word_embeddings (aliases lm_head when absent), and upcasts bf16 tensors into 64-byte-aligned NativeMemory.AlignedAlloc scratch owned by the weights. F32 tensors stay zero-copy mmap. ModelLoader.Load(path) auto-detects .gguf vs .safetensors. Architectures dispatched today: Llama, Mistral, Phi, Qwen; Mamba-3 continues to use the existing Mamba3WeightLoader path.

Test results: 10 new unit tests (HfConfigExtractor + synthetic-fixture TransformerSafetensorsLoadTests covering F32, bf16→F32 upcast, tied embeddings, missing-tensor error paths) + 1 new integration test (tiny-random Llama). hf-internal-testing/tiny-random-LlamaForCausalLM (~4 MB, 1M params, vocab=32000, hidden=16, 2 layers) downloads, loads, forward pass over [0, 1, 2] produces [3, 32000] logits, 96000/96000 finite, stddev ~0.08, in 58 ms. All 1175 existing unit tests stay green. Tokenizer ingest from tokenizer.json is deliberately out of scope here — it's its own effort.

@jamesburton

Copy link
Copy Markdown
Author

Stage D3 complete — Mamba-3 now loads and prefills end-to-end from a HuggingFace safetensors checkpoint. Mamba3TransformerModel : IModel composes the canonical Mamba3Block (8-slice in_proj, per-(T,H) ADT from dd_A, trap-gated scale, data-RoPE cumsum, attention-style SSD scan) into embed -> N x (RMSNorm + Mamba3Block + residual) -> final RMSNorm -> lm_head, and ModelLoader.LoadFromSafetensors now dispatches Architecture.Mamba3 alongside the dense Llama/Mistral/Phi/Qwen path. Scope is prefill only; each call allocates ephemeral per-layer ssm_state [H,P,N] and cum_angle [H,S] and discards them at return. Persistent decode state, tokenizer ingest, a generation loop, multi-shard safetensors, and real-checkpoint ib-ssm 370M validation (1.55 GB download, needs user approval) are the remaining follow-ups. MIMO checkpoints also need a loader extension for [H, R, N] B_bias / C_bias plus mimo_z / mimo_o; SISO checkpoints work today.

Tests: 8 new unit + 2 new integration, all green. Unit suite 1183 passed / 0 failed / 36 skipped (up from 1175 baseline). Forward-pass verification on tokens [0,1,2,3]: shape=[4, 16], finite=64/64, stddev=0.0356, [min=-0.071, max=0.071], 34 ms. Commit 989e0f9, branch feature/mamba-3 pushed.

@jamesburton

Copy link
Copy Markdown
Author

Agent C — Mixtral-family MoE: Dense-routing top-k Mixture-of-Experts support landed on feature/mamba-3. Introduces MoeConfig on ModelConfig, Architecture.Mixtral, and MoeSwiGluMlp kernel (full softmax over experts → stable top-k partial max-scan matching torch.topk → renormalise by sum per Mixtral convention → per-expert SwiGLU via existing FusedOps.SwiGLU → weighted combine). HfConfigExtractor detects num_local_experts/num_experts + num_experts_per_tok with optional moe_intermediate_size override (Phi-3.5-MoE). Safetensors loader resolves Mixtral tensor names (model.layers.{i}.block_sparse_moe.gate + experts.{j}.w[1-3]) with F16/BF16→F32 upcast. TransformerModel.Forward branches on per-layer Moe bundle — attention path unchanged. Out of scope: shared experts (DeepSeek-V3, old Qwen1.5-MoE), Qwen-MoE mlp.experts naming adapter, fused GroupedGEMM, expert parallelism, real Mixtral-8x7B validation.

Tests: Unit 1193 pass (+10: kernel stability/correctness/edge-cases, HfConfigExtractor Mixtral detection, synthetic safetensors loader forward-pass). Integration 97 pass, 5 skip, 0 fail — includes a new real-HF yujiepan/mixtral-tiny-random config-detection test (passes) and a forward-pass test (cleanly skips on upstream head_dim=1 RoPE incompatibility; synthetic fixture at head_dim=4 covers the full forward contract).

@jamesburton

Copy link
Copy Markdown
Author

Real-weight validation landed: end-to-end prefill through the 1.55 GB ib-ssm/mamba3-370M-10BT checkpoint on CPU (commit d2d7757). All 5 × 32 000 = 160 000 output logits are finite, per-position stddev is non-zero, argmax varies with input token — no loader crash, no NaN, no degeneracy.

Test results (Release build, Windows CPU):

LoadConfig_ReturnsExpectedDimensions          PASS (22 ms mmap)
  arch=Mamba3 vocab=32000 hidden=1024 layers=48 heads=32 head_dim=64
  d_state=128 d_in_proj=4480 num_rope_angles=32 rope_fraction=0.5
  is_mimo=False tied=False

ForwardProducesFiniteVocabLogits              PASS (2.8 s cold / 1.4 s warm for 5 tokens)
  Forward: shape=[5, 32000] finite=160000/160000
    pos[0] token=0    : min=-13.35 max= 8.17 mean=-5.06 stddev=3.19 argmax=29892
    pos[1] token=100  : min=-12.03 max= 7.71 mean=-3.80 stddev=2.78 argmax=29879
    pos[2] token=1000 : min=-13.08 max= 9.37 mean=-4.33 stddev=2.94 argmax=29891
    pos[3] token=10000: min=-12.75 max= 7.27 mean=-4.62 stddev=3.02 argmax=29889
    pos[4] token=31999: min=-11.70 max= 8.91 mean=-2.91 stddev=2.48 argmax=  292

ForwardMatchesCanonicalReference              SKIP (DOTLLM_IBSSM_REF_COMPARE unset)

How to reproduce (Windows, PowerShell):

$env:DOTLLM_IBSSM_CHECKPOINT_PATH = "C:/temp/dotllm-ibssm/model.safetensors"
dotnet test tests/DotLLM.Tests.Integration/DotLLM.Tests.Integration.csproj `
  -c Release --filter "FullyQualifiedName~IbSsmMamba3RealWeights"

The test also auto-detects C:/temp/dotllm-ibssm/model.safetensors or %USERPROFILE%/dotllm-ibssm-370m/model.safetensors if the env var is unset, and skips cleanly if none resolve. No production-code changes were needed — Mamba3WeightLoader already handled the [32, 1, 128] B_bias shape, the 1.5 GB mmap offsets compute correctly as long throughout, and Mamba3DataRoPE.ExecuteCanonical handles the partial-rotation case (rope_fraction=0.5num_rope_angles=32).

Canonical Python reference comparison deferred: the state-spaces/mamba forward path needs Triton+CUDA which isn't viable on Windows+CPU, and the pure-Python fallback is prohibitively slow at 370M dims (48 layers × 1024 hidden × 5 tokens). The algorithm-level comparators in Mamba3CanonicalReferenceCompareTests already validate the block math at tractable scales.

Next steps (separate PRs): tokenizer ingest (tokenizer.json already ships with the checkpoint), decode-loop with persistent SSM state across calls, MIMO-checkpoint loader extension.

@jamesburton

Copy link
Copy Markdown
Author

Mamba3State persistent state buffers (d42a808) — building block for streaming decode. New Mamba3State : IDisposable owns per-layer ssm_state [n_head, head_dim, d_state] + cum_angle [n_head, num_rope_angles] via NativeMemory.AlignedAlloc (64-byte). New overload Mamba3TransformerModel.Forward(tokens, positions, deviceId, Mamba3State) reads state at entry, writes back at exit; existing parameterless overloads route through a shared core with an ephemeral state, so behaviour is preserved. State threading is bit-exact deterministic across re-runs of the same chunk schedule (max_abs=0).

Honest framing: prefill+decode does NOT exactly equal one-shot at chunk boundaries — the canonical scan's shifted_γ[t] = DT[t+1]·(1-trap[t+1]) is a 1-token lookahead that drops to 0 at chunk edges (documented at length in Block_Canonical_DecodeSplit_MatchesReference). Closing that gap needs the canonical 4-buffer inference path (k_state + v_state in addition to the two threaded today) plus a streaming-decode SSD kernel. Both future stages — but Mamba3State and the Forward(..., state) overload are the substrate they'll reuse.

Tests: Mamba3State_ZeroedOnConstruct, StateThreading_IsDeterministic_AcrossRuns, StateThreading_ActuallyAdvances_NotZeroedPerCall, PrefillThenDecode_ApproximatesOneShot_WithExpectedDrift (pins drift bounds at 5e-5 abs / 1e-3 rel for the tiny synthetic), and on the real ib-ssm/mamba3-370M-10BT checkpoint: DecodeMatchesPrefillOnRealCheckpoint (finite logits, observed drift max_abs≈6.5 / argmax flip — within the documented chunk-edge regime, regression ceiling pinned at max_abs<100). 48 unit Mamba3 tests + 16 integration Mamba3 tests, all green. To run the real-checkpoint test locally: `$env:DOTLLM_IBSSM_CHECKPOINT_PATH = "C:/temp/dotllm-ibssm/model.safetensors"; dotnet test --filter FullyQualifiedName~IbSsmMamba3RealWeights`.

@jamesburton

Copy link
Copy Markdown
Author

Stage — HF tokenizer.json ingest (step 60b) landed on feature/mamba-3 (commit 3955065).

What's new

  • src/DotLLM.Tokenizers/Hf/HfTokenizerJsonParser.cs — parses vocab, merges (both "a b" and ["a","b"] forms), added tokens, Metaspace pre-tokenizer, ByteFallback decoder stages into an HfTokenizerSpec record.
  • src/DotLLM.Tokenizers/Hf/HfBpeTokenizerFactory.cs — maps merge rank to synthetic score (score = -rank) so the existing score-driven priority queue in SentencePieceEncoding reproduces HF's earliest-rank-wins merge order bit-for-bit — no changes to BpeTokenizer internals. Special added tokens (special: true) route through the control-token pre-split path; BOS/EOS auto-detected from <s> / </s> entries with Llama-2 fallbacks (1 / 2). Unsupported pre-tokenizers (ByteLevel, etc.) throw early — a separate adapter into CreateTiktoken will handle those.
  • ModelLoader.LoadTokenizerFromHfDirectory(dirOrFilePath) — surfaces the adapter next to LoadFromSafetensors without changing the (IModel, IDisposable, ModelConfig) tuple contract.

Tests

  • Unit: 10 new tests in HfBpeTokenizerTests.cs over a synthetic Llama-2-shaped vocab (parser primitives, ASCII round-trip, non-ASCII via byte fallback, special-token pre-split, BOS/EOS auto-detect, unsupported-pretokenizer rejection). All 229 tokenizer unit tests pass.
  • Integration: 3 new tests in IbSsmMamba3TokenizerEndToEndTests.cs, gated by DOTLLM_IBSSM_CHECKPOINT_PATH (or conventional / user-profile paths). Verified against the real 1.55 GB ib-ssm/mamba3-370M-10BT checkpoint:
    • "Hello world"[15043, 3186] — matches Llama-2 canonical token IDs exactly
    • "café" round-trips via byte fallback (bytes 0xC3 0xA9)
    • "The quick brown fox" → forward through 48-layer 370M SSM (3.8 s) → argmax id = 29892 → "," — end-to-end loop works without crashing

Docs

  • docs/ROADMAP.md: new step 60b under Phase 8, marked done.
  • README.md: News entry prepended.

Out of scope / next stage hazards

  • Byte-level (GPT-2) pretokenizer deferred — needs a separate adapter that routes into BpeTokenizer.CreateTiktoken.
  • Post-processor's BOS auto-insertion: caller's responsibility for now (the integration test feeds plain text, which is the common case for a base model).
  • Chat templates: unchanged — JinjaChatTemplate continues to own that path.

@jamesburton

Copy link
Copy Markdown
Author

Multi-shard safetensors loader (model.safetensors.index.json)a95e058

HuggingFace sharded-checkpoint support for models above the default 5 GiB shard cap (Llama-3-8B-Instruct, Mistral-7B-Instruct, …).

New public API (DotLLM.Models.SafeTensors):

  • ISafetensorsTensorSource — shared lookup surface (Tensors, TensorsByName, GetTensorPointer, GetTensorSpan, IDisposable). Implemented by both SafetensorsFile and MultiShardSafetensorsFile.
  • SafetensorsIndex — parses the weight_map + optional metadata.total_size sidecar. Parse(string), Load(string), DistinctShardFileNames().
  • MultiShardSafetensorsFile — owns one SafetensorsFile per shard, resolves tensor-name → owning shard, Open(indexPath) / Open(indexPath, index) / internal OpenWithoutIndex(...) for tests.

Consumers refactored (accept ISafetensorsTensorSource):

  • TransformerWeightsSafetensorsLoader.Load
  • Mamba3WeightLoader.Load
  • TransformerModel.LoadFromSafetensors
  • Mamba3TransformerModel.LoadFromSafetensors

Existing single-file callers are source-compatible — SafetensorsFile implements the new interface.

ModelLoader.LoadFromSafetensors(path) auto-detection:

  • directory → probe for model.safetensors.index.json first (multi-shard), else a single *.safetensors (single-shard);
  • index.json path → multi-shard;
  • single *.safetensors path → single-shard, but prefers a sibling model.safetensors.index.json when present.

Duplicate-name handling: the index is authoritative; tensors a shard declares that the index omits are absorbed; redeclarations that contradict the index throw with a clear tensor + shards error message.

Verification: resharded hf-internal-testing/tiny-random-LlamaForCausalLM into a 2-shard layout (4.1 MB + 19 KB + model.safetensors.index.json + config.json), loaded via ModelLoader.LoadFromSafetensors(directory), forward pass over [0, 1, 2][3, 32000] logits, 96000/96000 finite, mean=6.4e-05, stddev=0.0803. Layer-0 and layer-1 q_proj resolve to different shards as expected.

Tests: all 1229 unit tests green (11 new across SafetensorsIndexTests, MultiShardSafetensorsFileTests, plus a TransformerSafetensorsLoadTests.MultiShard_LlamaFixture_* 2-shard variant). All 5 safetensors integration tests pass; new MultiShardSafetensorsLoadTests uses a synthesis-on-first-run approach (single-file tiny-random Llama → resharded in-place) so the test skips gracefully when offline.

Docs: docs/ROADMAP.md new Phase 2 step 20c (:white_check_mark:), Phase 2 count bumped 11/11 → 12/12 in README; News entry added.

@jamesburton

Copy link
Copy Markdown
Author

Mamba-3 end-to-end text generation now works on real ib-ssm/mamba3-370M-10BT. The full pipeline — ModelLoader.LoadFromSafetensors + ModelLoader.LoadTokenizerFromHfDirectory + iterative Mamba3TransformerModel.Forward (growing-context one-shot prefill per step) + argmax + tokenizer decode — composes correctly. New gated integration test IbSsmMamba3GenerationTests.Mamba3_GeneratesText_FromTokenizedPrompt generates 5 tokens from "The capital of France is"[263, 1407, 4100, 760, 310]"The capital of France is a very important part of" at ~1.4 s/token (7 s total end-to-end on CPU). Each step re-runs full prefill to sidestep the shifted_γ chunk-edge lookahead drift documented under step 60a — O(N²) total but every forward is canonical. Open follow-ups: streaming SSD for O(N)-per-token decode (needs k_state + v_state threading plus a streaming-decode kernel that consumes them, then the chunk-edge drift closes and Mamba3State-threaded decode becomes bitwise-equivalent to one-shot). Test-only change — no production code touched.

jamesburton referenced this pull request in jamesburton/dotLLM Apr 20, 2026
Extends the Mixtral MoE plumbing introduced in step 58 to the HuggingFace
Qwen-MoE convention: `mlp.gate` + `mlp.experts.{j}.{gate_proj,up_proj,down_proj}`
tensor names instead of Mixtral's `block_sparse_moe.gate` + `experts.{j}.w1/w2/w3`,
optional shared-expert branch (Qwen1.5-MoE-A2.7B: `mlp.shared_expert.*` +
optional `mlp.shared_expert_gate.weight` sigmoid scalar), per-layer MoE vs
dense dispatch (Qwen3-MoE `decoder_sparse_step=2`), and the `norm_topk_prob=false`
raw-softmax gating used by Qwen1.5-MoE.

New `Architecture.QwenMoe` enum variant. `MoeConfig` gains five fields:
`NormTopKProb`, `SharedExpertIntermediateSize`, `HasSharedExpertGate`,
`DecoderSparseStep`, `MlpOnlyLayers`, plus an `IsMoeLayer(layerIdx)` helper.
`HfConfigExtractor` detects `model_type=qwen{2,3}_moe` and
`architectures[0]=Qwen{2,3}MoeForCausalLM`, surfacing all of the above from
the HF config.json. `MoeSwiGluMlp.ExecuteWithSharedExpert` adds a dense
SwiGLU shared-expert branch (optionally scaled by `sigmoid(hidden . shared_expert_gate)`)
and a `normTopKProb` flag — the existing Mixtral `Execute` overload is
unchanged. `TransformerWeightsSafetensorsLoader.LoadQwenMoeLayer` resolves
the Qwen-convention tensors with BF16/F16 → F32 upcast.

Verified:
- 3 new `MoeSwiGluMlp` kernel unit tests against a hand-rolled reference.
- 4 `HfConfigExtractor` Qwen-MoE detection tests (Qwen3-MoE,
  Qwen1.5-MoE-A2.7B with shared expert + `norm_topk_prob=false`,
  `mlp_only_layers` override).
- 2 synthetic-fixture forward-pass tests (Qwen-MoE plain + shared expert).
- Real `yujiepan/qwen3-moe-tiny-random` checkpoint (~20 MB, 2 layers,
  8 experts, top-2, `decoder_sparse_step=2`) — detect + load + 3-token
  forward, finite logits with nonzero variance. Gated integration test.
- All existing Mixtral unit + integration tests still pass (0 regressions).

Out of scope (follow-up):
- DeepSeek-V2/V3 multi-shared-expert (`n_shared_experts > 1`) + MLA.
- Real Qwen1.5-MoE-A2.7B validation (~14 GB, infeasible on CI).
- Fused GroupedGEMM and expert parallelism.

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

Copy link
Copy Markdown
Author

Qwen-MoE naming + shared expert support (4b1029b)

Extends the Mixtral MoE plumbing (step 58) to the HuggingFace Qwen-MoE convention.

Tensor-name support (Qwen1.5 / Qwen2 / Qwen3-MoE):

  • Router: model.layers.{i}.mlp.gate.weight
  • Routed experts: model.layers.{i}.mlp.experts.{j}.{gate_proj,up_proj,down_proj}.weight
  • Optional shared expert: model.layers.{i}.mlp.shared_expert.{gate,up,down}_proj.weight
  • Optional sigmoid gate: model.layers.{i}.mlp.shared_expert_gate.weight

New config surface (MoeConfig):

  • NormTopKProb — Mixtral/Qwen3-MoE renormalise top-k to sum=1; Qwen1.5-MoE leaves raw softmax values.
  • SharedExpertIntermediateSize + HasSharedExpertGate — the dense parallel branch in Qwen1.5-MoE-A2.7B.
  • DecoderSparseStep + MlpOnlyLayers + IsMoeLayer(i) helper — Qwen3-MoE interleaves dense and MoE layers (decoder_sparse_step=2 → layer 0 dense, layer 1 MoE).

Kernel: MoeSwiGluMlp.ExecuteWithSharedExpert adds the shared branch + sigmoid gate + normTopKProb flag. Existing MoeSwiGluMlp.Execute call sites untouched.

Loader: LoadQwenMoeLayer sibling to LoadMixtralMoeLayer; per-layer dispatch in TransformerWeightsSafetensors — Qwen-MoE dense layers fall through to the Llama-style mlp.{gate,up,down}_proj path.

Verified:

  • 3 new MoeSwiGluMlp kernel tests (routed+shared-no-gate, routed+shared+sigmoid-no-renorm, shared-disabled byte-identity with Mixtral path).
  • 4 HfConfigExtractor Qwen-MoE detection tests (Qwen3-MoE tiny-random config, Qwen1.5-MoE-A2.7B shared expert + norm_topk_prob=false, mlp_only_layers override).
  • 2 synthetic-fixture forward-pass tests (Qwen-MoE plain + Qwen-MoE with shared expert).
  • Real yujiepan/qwen3-moe-tiny-random (~20 MB, 2 layers × 8 experts × top-2, decoder_sparse_step=2): detection + load + 3-token forward → finite logits, nonzero variance.
  • All existing Mixtral tests green (0 regressions).

Out of scope (follow-ups):

  • DeepSeek-V2/V3 multi-shared-expert (n_shared_experts > 1) + MLA attention.
  • Real Qwen1.5-MoE-A2.7B validation (~14 GB, infeasible on CI).
  • Fused GroupedGEMM + expert parallelism.

Pushed 5e723f9..4b1029b on feature/mamba-3.

jamesburton referenced this pull request in jamesburton/dotLLM Apr 20, 2026
…llocations

Introduces Mamba3ForwardScratch : IDisposable mirroring the existing
NemotronHForwardState pattern: 13 named 64-byte-aligned NativeMemory.AlignedAlloc
buffers (Proj, X, Z, Dt, Adt, Trap, Gamma, Scale, AnglesRaw, B, C, QkPreDot,
YScan) owned by Mamba3TransformerModel, grown power-of-two on demand via
EnsureCapacity(seqLen), reused across every layer of every Forward call.

Mamba3Block.Forward and Mamba3Block.ForwardMimo now take the scratch as a
required first parameter instead of allocating ~10 float[] per call. On the
ib-ssm 370M checkpoint this collapses ≈480 managed allocations per step
(48 layers × ≈10 buffers) to zero after the first Forward — a genuine
zero-GC hot path.

Scratch sizes B / C for max(1, mimoRank) so one instance serves both SISO
and MIMO paths. Constructor takes ModelConfig (production) or raw dimensions
via FromDimensions (kernel-level tests). ComputeMemoryBytes now reports the
scratch footprint.

Numerics preserved bit-exact: all 57 Mamba-3 unit tests + 20 integration
tests still pass, including the canonical Python-reference SISO/MIMO block
comparators and all 9 real-weight ib-ssm/mamba3-370M-10BT tests
(ForwardProducesFiniteVocabLogits, DecodeMatchesPrefillOnRealCheckpoint,
Mamba3_GeneratesText_FromTokenizedPrompt, …).

9 new Mamba3ForwardScratchTests exercise lifecycle: zero-capacity lazy init,
power-of-two growth, idempotent Dispose, accessor-after-Dispose throws, and
accessor-beyond-capacity throws.

Roadmap step 60d. (#136)

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

Copy link
Copy Markdown
Author

Mamba-3: pooled forward scratch (zero-GC on the hot path)

Introduces Mamba3ForwardScratch : IDisposable mirroring the existing NemotronHForwardState pattern: 13 named 64-byte-aligned NativeMemory.AlignedAlloc buffers (Proj, X, Z, Dt, Adt, Trap, Gamma, Scale, AnglesRaw, B, C, QkPreDot, YScan) owned by Mamba3TransformerModel, grown power-of-two on demand via EnsureCapacity(seqLen), reused across every layer of every Forward call. Scratch sizes B/C for max(1, mimoRank) so one instance serves both SISO and MIMO paths.

Mamba3Block.Forward / ForwardMimo now take the scratch as a required first parameter. On the ib-ssm/mamba3-370M-10BT checkpoint this collapses ≈480 managed float[] allocations per generation step (48 layers × ≈10 buffers) to zero after the first Forward.

Numerics preserved bit-exact: all 57 Mamba-3 unit tests + 20 integration tests pass, including the canonical Python-reference SISO/MIMO block comparators and all 9 real-weight ib-ssm tests (ForwardProducesFiniteVocabLogits, DecodeMatchesPrefillOnRealCheckpoint, Mamba3_GeneratesText_FromTokenizedPrompt, …). 9 new lifecycle unit tests exercise power-of-two growth, idempotent Dispose, and bounds checks.

Roadmap step 60d. Commit 5375b66.

@jamesburton

Copy link
Copy Markdown
Author

Mamba-3 streaming decode — shifted_γ chunk-edge drift closed

Commit: 12f9a19 (pushed). Range: 5375b66..12f9a19.

What changed

Mamba3State gains two per-layer persistent buffers matching canonical state-spaces/mamba commit 7438488:

  • k_state [n_head, d_state] — previous chunk's last-token post-RoPE, pre-scale K
  • v_state [n_head, head_dim] — previous chunk's last-token V (= x)

Mamba3Block.Forward gains optional kState / vState span parameters. When non-empty:

  • At chunk start: ssm_state += v_state · k_state · DT[0] · (1 - trap[0]) (pre-scan) — folds in the deferred boundary contribution that one-shot would have produced via scale[T_prev-1] = γ + shifted_γ. Matches mamba3_siso_fwd.py:341-352.
  • At chunk end: persists bHRN[T-1]kState and xBuf[T-1]vState. Matches mamba3_siso_fwd.py:318-322 (pre-scale K) and the v = x convention in mamba3_siso_step.

An overload without k/v spans keeps one-shot callers unchanged.

Mamba3TransformerModel.Forward(..., Mamba3State) threads all four state buffers (ssm_state, cum_angle, k_state, v_state) through every layer.

Numerical results

Tiny synthetic fixture (2-layer, 4-head, d_state=8):

Schedule Before (max_abs) After (max_abs)
2+2 5e-7 1e-8
2+1+1 2.5e-6 2e-6
1+1+1+1 n/a 2e-6

Real ib-ssm/mamba3-370M-10BT (48 layers, 32 heads, d_state=128, 2-prefill + 1-decode):

Before After
Last-token max_abs 6.5 3.3
Last-token max_rel 2e4 5e4
Top-1 argmax match NO (prefill=29891, split≠29891) YES (both 29891)

The argmax match is the user-visible win: next-token sampling is now consistent between prefill(3) and prefill(2) + decode(1). Previously identical prompts could generate different tokens depending on whether generation used one-shot prefill or streaming.

Remaining 3.3 max_abs is F32 accumulation noise across 48 layers of (GEMM + SSD + GEMM + residual). An F32 reorder envelope, not a math bug.

Tightened assertions

  • PrefillThenDecode_ApproximatesOneShot_WithExpectedDriftPrefillThenDecode_BitEqualsOneShotabs_tol 5e-5 → 1e-5, extended schedules (added 1+1+1+1)
  • DecodeMatchesPrefillOnRealCheckpoint ceiling 100f10f with a new Assert.Equal(argmaxPrefill, argmaxSplit)
  • New Mamba3State_ZeroedOnConstruct assertions cover KState / VState
  • New StateThreading_KAndVStateAdvance_AtChunkEnd regression-guards the post-scan copy-back

New tests

Mamba3BlockStreamingTests (kernel-level, 7 split schedules): 2+2+2, 1+1+1+1+1+1, 3+3, 4+2, 2+4, 1+2+3, 5+1. Each asserts y, ssm_state, k_state, v_state all reproduce one-shot within abs_tol=1e-5 / rel_tol=1e-4. All pass.

Test suite

  • 65 Mamba-3 unit tests (was 57) — all green
  • 20 Mamba-3 integration tests — all green
  • 1254 unit + 108 integration suite-wide — all green

MIMO streaming deferred

Canonical MIMO does not thread input states through its combined kernel — only the step() single-token decode supports MIMO state resumption. The tilelang MIMO forward has no input_k_state / input_v_state signature (mamba_mimo_forward call site in mamba3.py:94-101). Our MIMO Block path is unreachable from Mamba3TransformerModel today (throws NotSupportedException pending full MIMO checkpoint support, tracked separately). All currently-validated Mamba-3 checkpoints (ib-ssm 370M) are SISO.

Performance implication

With this plumbing the groundwork is in place for true O(N) streaming-decode generation. Today generation still uses growing-context prefill (step 60c — O(N²) total) but the next step (generation loop rewire to single-token decode + streaming state) no longer has a math blocker.

@jamesburton

Copy link
Copy Markdown
Author

MLA attention kernel (PoC) + DeepSeek-V2/V3 detection (9a324e6). Scalar standalone kernel MlaAttention.Execute implements DeepSeek's Multi-head Latent Attention end-to-end: hidden → Q LoRA factorisation (q_a_projq_a_layernormq_b_proj, or monolithic Q when q_lora_rank=0), KV compression via kv_a_proj_with_mqakv_a_layernormkv_b_proj (split K_nope/V), decoupled RoPE on qk_rope_head_dim sub-dim with MQA-shared K_rope, per-head causal SDPA with scale 1/sqrt(qk_nope + qk_rope), o_proj. Architecture.{DeepSeekV2, DeepSeekV3} enum variants + HfConfigExtractor detects DeepseekV{2,3}ForCausalLM / model_type=deepseek_v{2,3} and populates MlaConfig (KvLoraRank, QLoraRank, QkNopeHeadDim, QkRopeHeadDim, VHeadDim, RopeTheta, YaRN scaling fields) + DeepSeek-flavoured MoeConfig (n_routed_experts, n_shared_experts, first_k_dense_replace). Verified against yujiepan/deepseek-v2-tiny-random HF checkpoint (config detection + NotSupportedException contract in current TransformerModel path) and a reference implementation within 5e-4 absolute tolerance across 4 kernel tests.

Out of scope / follow-ups (explicitly documented in code): TransformerModel forward integration with the existing R4-interleaved/FusedDecodeGemv/quantised-KV-cache decode path, MlaWeightsLoader for DeepSeek tensor names (q_a_proj / q_a_layernorm / q_b_proj / kv_a_proj_with_mqa / kv_a_layernorm / kv_b_proj), latent KV-cache (512 vs 3072 per head), YaRN mscale correction, absorption optimisation (W_q_nope @ W_k_nope^T fused), multi-shared-expert (n_shared_experts > 1). ROADMAP step 48 ticked. Tests: 4 new kernel + 3 new HfConfigExtractor + 2 new integration (tiny-random DeepSeek-V2 download gated). 1261 unit + full integration suite green.

@jamesburton

Copy link
Copy Markdown
Author

MoE GroupedGEMM refactor (17f0910) — MoeSwiGluMlp.Execute / ExecuteWithSharedExpert rewritten from per-token per-expert scalar loop to gather/batched-GEMM/scatter. Bucket tokens by their top-k-assigned experts (ArrayPool<int> index lists, zero sustained allocations), run one batched SwiGLU per expert across all its assigned tokens (gather [B, hidden]w1/w3 GEMM → silu*up → w2 GEMM), scatter back preserving the original per-(token, slot) accumulation order. Public signatures unchanged — both Mixtral and Qwen-MoE paths route through the same grouped core; shared-expert branch runs once per token on the un-gathered seqLen batch and applied after the routed sum.

Bit-identical to the prior scalar path. Verified against all 8 existing MoeSwiGluMlp unit tests (Mixtral 2-expert top-1, 4-expert top-2, Qwen-MoE with + without shared expert + with + without sigmoid gate + norm_topk_prob on/off) and all 16 MoE integration tests including the real yujiepan/qwen3-moe-tiny-random (~20 MB) end-to-end forward (detection → load → 3-token forward → finite logits). Zero test regressions.

Follow-ups: fused GroupedGEMM with SIMD-batched SwiGLU for another ~2-3× on top; expert parallelism; GPU port.

@jamesburton

Copy link
Copy Markdown
Author

Mamba-3 MIMO streaming decode (commits 85c3164, c68e3f0, 48cc32a) — extends step 60e's SISO streaming to MIMO (rank > 1) at the kernel + block level.

State shapes (confirmed from canonical mamba3.py:434-452 allocate_inference_cache): ssm_state [H, P, N] (rank summed into state in kernel), cum_angle [H, S] (rank-free), k_state [R, H, N] for MIMO / [H, N] for SISO (extended), v_state [H, P] (rank-free, V is not rank-expanded).

Boundary derivation: canonical mamba3_mimo_combined (tilelang) does NOT accept input states; derived the streaming adjustment from the kernel's state-update structure, which sums K over rank: ssm[h,p,n] += v_state[h,p] · (Σ_r k_state[r,h,n]) · DT[0,h] · (1-trap[0,h]) — the rank-sum analog of SISO's boundary term.

New Mamba3CanonicalSsd.ExecuteMimoStreaming mirrors ExecuteSisoStreaming's contract but rank-aware. Mamba3Block.ForwardMimo gains a streaming overload (empty-span preserves one-shot). Empty-span and single-chunk [T] schedules are bit-identical to the existing one-shot ExecuteMimo — the pre-existing Block_Canonical_Mimo_MatchesReference fixture comparator continues to pass.

Synthetic MIMO drift (R=3, T=6, H=4, P=4, N=8): y max_abs ≤ 4.7e-15, ssm_state max_abs ≤ 3.4e-13 across [6], [3,3], [2,2,2], [1,1,1,1,1,1], [4,2], [2,4], [5,1], [1,2,3] — pure F32-reorder noise, orders of magnitude tighter than the 1e-5/1e-4 gate.

Tests: 83 Mamba-3 unit tests (was 65; +18 new — 8 kernel + 9 block + 1 state shape), 20 integration tests, all passing.

Deferred: Mamba3TransformerModel still throws NotSupportedException for MIMO — kernel + block paths are complete and test-covered, but the weight loader (Mamba3WeightLoader) doesn't yet handle [H, R, N] B_bias/C_bias or mimo_x / mimo_z / mimo_o tensors. Once the loader lands, model-level plumbing is a one-liner.

@jamesburton

Copy link
Copy Markdown
Author

DeepSeek multi-shared-expert MoE (5aafc3c) — extends the MoE shared-expert branch to run N shared experts in parallel (DeepSeek-V2/V3 convention n_shared_experts > 1; Qwen1.5-MoE at N=1 unchanged, bit-identical).

Config/weights: MoeConfig.NumSharedExperts (default 1). MoeLayerWeights.Shared{Gate,Up,Down}Proj migrated from single nint to nint[]. HasSharedExpert checks both the intermediate size and the array length.

Kernel: MoeSwiGluMlp.ExecuteWithSharedExpert now takes ReadOnlySpan<nint> per shared-proj kind. For k=0 the GEMM writes directly into the down-buffer (preserving bit-identity with the prior single-shared code path); subsequent experts compute into pooled scratch then TensorPrimitives.Add into the down buffer. Sigmoid shared_expert_gate (Qwen1.5-MoE) only applies when NumSharedExperts=1 && HasSharedExpertGate.

Loader: plural HF naming model.layers.{i}.mlp.shared_experts.{k}.{gate,up,down}_proj loaded into the array; singular mlp.shared_expert.* still loads as length-1 array.

Status: DeepSeek-V2/V3 still throw NotSupportedException in ModelLoader — MLA TransformerModel integration is the remaining blocker. MoE loader path is ready for when MLA lands.

Tests: 2 new kernel tests (multi-shared sum validated vs scalar ref + length-1-array bit-identity with prior single-shared), 1 new synthetic-fixture safetensors-loader test (DeepSeekStyleMoE_PluralSharedExperts_LoadsAndProducesFiniteLogits). 1282 unit tests pass (up from 1280), 0 failures. Mixtral + Qwen-MoE paths byte-identical to prior.

@jamesburton

Copy link
Copy Markdown
Author

DeepSeek-V2/V3 end-to-end MLA integration (3757325) — wires the MLA kernel (PR 9a324e6) into TransformerModel.Forward and adds the DeepSeek safetensors weight loader. ModelLoader.LoadFromSafetensors now dispatches Architecture.DeepSeekV2/DeepSeekV3 — no more NotSupportedException.

Weights: new MlaLayerWeights class (referenced via TransformerLayerWeights.Mla) holds Q{A,B}Proj / QProj (monolithic when q_lora_rank=0), KvAProjWithMqa / KvBProj, Q/KvALayernormWeight, and per-layer MLA dims. LoadDeepSeekMlaLayer resolves all attention tensors; FFN routes to dense MLP (first-K prefix) or LoadQwenMoeLayer (plural shared_experts — step 58c already supported).

Attention branching: TransformerModel.Forward adds if (lw.Mla is not null) { RMSNorm → MlaAttention.Execute → residual → goto FfnBranch; } — jumps past the GQA Q/K/V/RoPE/Attention/O code to the shared FFN dispatch, preserving Llama/Mistral/Phi/Qwen path untouched.

Verified against real yujiepan/deepseek-v2-tiny-random (DeepSeekV2, MLA, hidden=8, layers=2, heads=2, qk_head=4, vocab=102400, kv_lora_rank=2, q_lora_rank=2): 4-token prefill produces [4, 102400] logits, 409600/409600 finite, stddev 0.056, argmax varies per position. TinyDeepseekMlaSafetensorsLoadTests flipped from NotSupportedException contract to real forward-pass + finite-logit assertion. 3 new unit tests (LoRA-Q prefill, monolithic-Q prefill, LoRA-Q single-token). 1285 unit tests pass.

PoC scope: KV-cache reruns the full MLA forward per call (no caching yet). Latent KV-cache optimisation (store kv_lora_rank=512 latent instead of full per-head K/V — ~6× KV memory savings), YaRN mscale correction, and absorption (W_q_nope @ W_k_nope^T fused at load time) remain follow-ups. Real DeepSeek-V2-Lite validation (~16 GB download) also deferred.

@kkokosa

kkokosa commented Apr 21, 2026

Copy link
Copy Markdown
Owner

Looks great! I'm OOF sick, will take a lot in a few days. Thanks for the work 💪

@jamesburton

Copy link
Copy Markdown
Author

Phi-3.5-mini + Granite-3.0-MoE loading real weights end-to-end. Two loader extensions landed:

  • Phi-3 fused tensors (006935b) — self_attn.qkv_proj [Q+K+V, hidden] and mlp.gate_up_proj [2*intermediate, hidden] split at load time via new SplitFusedProjection(file, name, partRows[], ...) helper. One 64-byte-aligned F32 buffer per split, BF16/F16/F32 decoded in place. Non-fused paths unchanged.
  • Granite-MoE fused-per-expert (6c8c662) — block_sparse_moe.input_linear [E, 2*I, H] + output_linear [E, H, I] + router.layer gate. New Architecture.GraniteMoe variant. LoadGraniteMoeLayer slices each expert's w1 (rows [0:I]) / w3 (rows [I:2I]) / w2 slab at element offsets e*(2*I*H) / e*(H*I) into per-expert F32 pointers; MoeSwiGluMlp handles top-8 gating unchanged.

Real-weight validation:

Model Size Shape Finite stddev Load Forward
microsoft/Phi-3.5-mini-instruct 7.6 GB [3, 32064] 96192/96192 7.00 35.9 s 4.8 s
ibm-granite/granite-3.0-3b-a800m-instruct 6.3 GB [3, 49155] 147465/147465 16.13 37.6 s 2.9 s

Test harness RealHfSafetensorsEndToEndTests now covers both (env vars DOTLLM_PHI35_CHECKPOINT_PATH / DOTLLM_GRANITE3_CHECKPOINT_PATH, auto-detect of C:/temp/dotllm-{phi35-mini,granite3-moe}/). No kernel changes — pure weight-pathway work. 1285 unit tests pass; full integration suite green except one pre-existing Mamba-3 generation-test timing ceiling (unrelated).

@jamesburton

jamesburton commented Apr 22, 2026 via email

Copy link
Copy Markdown
Author

Your Name added 8 commits April 23, 2026 16:08
DeepSeek-V2/V3 extend context length via YaRN, which adds both a RoPE
frequency rescaling and a softmax-scale multiplier mscale² where
mscale = 0.1 * mscale_all_dim * log(factor) + 1.0 (per HF
modeling_deepseek.yarn_get_mscale). For DeepSeek-V2-Lite this is
~1.59 — without it, attention scores are 37% too small and long-context
logits drift.

- MlaConfig.ComputeYarnSoftmaxScaleMultiplier() returns mscale² when
  factor > 1 and mscale_all_dim != 0, else 1.0f. Uses mscale_all_dim
  (not mscale) per the HF softmax_scale correction — the mscale field
  governs RoPE frequency rescaling, not wired yet.
- MlaAttention.Execute gains an optional attnScaleMultiplier parameter
  (default 1.0f) folded into the attention scale before softmax.
- TransformerModel.Forward passes Config.MlaConfig.ComputeYarn... at
  the MLA kernel call site. When the checkpoint has no YaRN scaling
  (pre-V2 or V2-Lite without context extension) this is 1.0f and the
  path is bit-identical with pre-P2.2.

Tests: 5 MlaConfig formula tests + 1 MlaAttention scale-multiplier
test (1321/0/36 unit pass, +6 vs baseline). DeepSeek-V2-Lite
real-weight e2e still passes (3m 19s).

RoPE frequency rescaling (the other half of YaRN, needed for context
lengths well past original_max_position_embeddings) is deferred to a
follow-up — the softmax correction is the primary per-token fix and
applies uniformly across all positions, whereas RoPE rescaling kicks
in only beyond the original training window.
Got accidentally added in the previous commit via git add -A; it's a
Claude session coordination file with no source content. Add to
.gitignore so this doesn't recur.
Prevents the session-local Claude coordination lock file from being
re-added by 'git add -A' in future commits (the prior 7f9f40f untracked
it; this makes the exclusion durable).
Adds HfLegacyBpeLoader parsing the legacy HF trio (vocab.json +
merges.txt + tokenizer_config.json) into the existing HfTokenizerSpec,
and wires it as a fallback in HfBpeTokenizerFactory.TryLoadFromDirectory.
Promotes scalar bos/eos/unk/pad_token fields to special added tokens
when the checkpoint lacks added_tokens_decoder entries for them.

Unblocks Granite-3.0 tokenizer loading (and GPT-2 proper, and any
other ByteLevel-family repo that ships only the legacy format). 8 new
unit tests: 5 synthetic + 3 gated against C:/temp/dotllm-granite3-moe/.

Follow-ups flagged:
- HfBpeTokenizerFactory.FindByteLevelBos doesn't recognise Granite-3's
  '<|end_of_text|>' special (pre-existing, not legacy-specific)
- SentencePiece tokenizer.model path still returns null from
  TryLoadFromDirectory; requires proto parser, separate work item

From worktree agent-ace55cf2.
Under xUnit's default collection-level parallelism, concurrent
HfLegacyBpeLoader.TryLoad calls against the 442 KB merges.txt at
C:/temp/dotllm-granite3-moe/ deadlocked the full suite (hangs for
hours; the three real-Granite SkippableFact tests pass standalone in
0.9s). Adding a SequentialFileIO collection with DisableParallelization
and tagging the class fixes the hang — tests now complete 1319/0/36
in ~2s.

A testhost-process crash occurs at end-of-run after all tests have
finished executing; exit code is still 0 and no test fails. Tracked
as a follow-up (likely a CUDA-test-fixture cleanup issue, unrelated
to the tokenizer tests).
Adds persistent per-layer K_nope / V / K_pe storage to MLA forward.
Before: every Forward() recomputed K/V from scratch for all tokens
(O(N²) prefill cost, O(N) per decode step). Now the kernel appends the
new seqLen rows into a native per-layer store at offset cachedLength
and attends over all cachedLength + seqLen positions.

Design per research (vLLM-style three phases, correctness first):
  Phase A — expanded K/V cache (this commit). Bit-identical oracle
            for Phase B. Does NOT compress to latent yet; stores the
            fully expanded per-head K_nope / V.
  Phase B — latent [kv_lora_rank + qk_rope_head_dim] cache + W_UK_T
            at-decode multiplication (the ~8× memory win).
  Phase C — prefill-expand / decode-absorbed split per vLLM's MLA
            backend.

Decisive correctness check: two new tests prefill N tokens, decode M
tokens one at a time, and assert each row matches a single-call
forward over all N+M tokens within 1e-4 at F32. Covers both
LoRA-factored Q (DeepSeek-V2/V3) and monolithic Q (V2-Lite).

- MlaExpandedKvState: per-layer native K_nope/V/K_pe, 64-byte aligned,
  single-stream (not re-entrant; beam/batch needs per-sequence state).
  Deliberately NOT an IKvCache — qk_head_dim ≠ v_head_dim breaks the
  uniform-head-dim assumption and K_pe is shared across heads, which
  neither GQA nor MHA caches model.
- MlaAttention.Execute: four optional native-pointer params
  (cachedKNope / cachedV / cachedKPe / cachedLength). Defaults (all 0)
  preserve the cache-less PoC path bit-identically; 13 existing MLA
  unit tests still pass unchanged.
- TransformerModel: lazily allocates MlaExpandedKvState on first MLA
  forward, resets when positions[0] == 0 (fresh sequence), advances
  by seqLen after each layer. Caller's IKvCache is still ignored for
  MLA layers — documented in the branch comment.

DeepSeek-V2-Lite real-weight end-to-end: passes in 2m 11s
(~35% faster than the pre-cache 3m 19s baseline, even on a single-shot
prefill — the pre-allocated native buffers avoid per-layer managed
array churn).
SUPPORTED_MODELS.md:
- DeepSeek-V2 row: 'tiny-random' → 'real weights' (DeepSeek-V2-Lite
  Load+Forward landed at 5ff5312). YaRN softmax mscale² now applied
  (3091944). Phase A cache note added for MLA.
- Mamba-3 row: drop 'MIMO blocked' caveat — P0.3 landed, loader +
  dispatch + MIMO forward test all in place (0499465).
- Per-architecture MLA section: describe Phase A / Phase B split
  explicitly so readers know the current correctness-first state.

KV_CACHE.md:
- New 'MLA KV-Cache' section covering why it can't ride IKvCache
  (qk_head_dim ≠ v_head_dim + shared K_pe), Phase A expanded layout
  with per-layer sizes, and Phase B design (latent compression +
  W_UK absorption) with the 7.2× memory reduction and the 1e-3
  drift-vs-Phase-A acceptance bar.
Fix the lone Debug-build CS8600 warning in CudaKernelTests.cs(27,25):
FindPtxDir() returns string?, so ptxDir must be declared string? (not
string) to match. No logic change — the subsequent null check stays.

Release build is already warning-clean.

Warning counts (no-incremental rebuild):
  Debug: 1 -> 0
  Release: 0 -> 0
Your Name and others added 9 commits April 28, 2026 09:34
Adds two new test files covering the runtime LoRA path:

LoraForwardParityTests:
- LoraDelta_MatchesScalarReference: verifies the LoRA kernel matches
  a hand-rolled scalar (sum_i x_i * B_ri) + (sum_r A_or * tmp_r)
  reference implementation at abs 5e-3 / rel 1e-3 — the primary kernel
  correctness anchor.
- LoraDelta_ZeroBIsNoOp: with B=0 the delta is exactly zero (sanity).
- Forward_NoAdapter_VsZeroAdapter_AreIdentical: builds a tiny 2-layer
  Llama via the synthetic safetensors fixture, runs forward without
  an adapter and with an all-zero adapter, asserts elementwise close.
  This confirms the LoRA-aware Forward path does not perturb output
  when the adapter contributes no delta.
- Forward_NonZeroAdapter_ProducesMeasurableDelta: same model, with a
  random adapter on q_proj/v_proj, verifies the logits differ by more
  than 1e-3 — proves the LoRA path is actually firing rather than a
  silent no-op.

LoraAdapterRegistrySwitchTests:
- Switch_BetweenAdapters_ProducesDifferentOutputs: loads two adapters
  with distinct seeds, runs forward(A) then forward(B), asserts (a)
  the swap completes well under the Phase 7 100 ms target via
  Stopwatch and (b) outputs differ measurably.
- Registry_LoadGetUnload_RoundTrip: covers the LoraAdapterRegistry
  duplicate-load rejection, missing-key returns null, list/unload
  semantics, and Dispose chaining.

Full unit suite: 1740 / 1740 passing (157 skipped — GPU/HW-dependent
tests unchanged). All previously-existing Mamba-3, NemotronH,
DeepSeek-MLA, MoE, and standard-transformer forward tests pass
unchanged when no adapter is supplied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the real-adapter integration test gated on a HuggingFace download
of llamafactory/tiny-random-Llama-3 (~8 MB base) plus
llamafactory/tiny-random-Llama-3-lora (~27 KB PEFT adapter — rank 8,
alpha 16, targets q/k/v/o/gate/up/down). Both repos are public, no
auth gate, no special license. The test:

1. Resolves base + adapter from the conventional cache layout
   (~/.dotllm/test-cache/<org>/<repo>/) — same pattern as existing
   TinyLlamaSafetensorsLoadTests — and downloads on first run via
   HuggingFaceDownloader.
2. Skips gracefully (SkippableFact) when the network is offline,
   rate-limited, or the repos are removed from the Hub.
3. Loads the base via ModelLoader.LoadFromSafetensors and the adapter
   via PeftAdapterLoader.LoadFromDirectory (validates shape against
   the loaded ModelConfig — fails fast on mismatch).
4. Runs forward without and with the adapter on the same input, asserts
   all 384 768 logit values are finite, and asserts maxAbsDiff > 1e-5
   so the LoRA path is definitely contributing rather than silently
   no-op.
5. Times Forward(adapter) via Stopwatch (~72 ms locally for 14 adapted
   sites x 2 layers — well under the Phase 7 100 ms swap target).

Local run output:
  Adapter: rank=8 alpha=16
    target_modules=[up_proj, v_proj, down_proj, gate_proj, k_proj, q_proj, o_proj]
    adapted_layer_count=14
  Forward(adapter) took 72.26 ms
  Finite=384768/384768  maxAbsDiff=0.0740389

Existing focused integration smoke tests (TinyLlama, TinyDeepseek-MLA,
TinyMamba-3, TinyQwen-MoE forward + load) all pass unchanged when no
adapter is supplied — 9 passed / 1 skipped (Mixtral cache miss).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the device-side mirror of an ILoraAdapter for the Vulkan backend.

VulkanLoraAdapter wraps the runtime ILoraAdapter abstraction and uploads
each per-(layer, projection) (B, A) factor pair as a pair of F32 row-major
device-local buffers. The runtime scaling factor scale = alpha / rank is
folded into the B (down-projection) weight at upload time so the Vulkan
LoRA delta path can compose existing kernels (matmul_f32 + add) without
needing a new "scaled add" shader — Option A per the Phase 4b brief.

VulkanLoraAdapterCache keeps a ConcurrentDictionary keyed by ILoraAdapter
reference identity so repeat forwards with the same adapter pay zero
upload cost; the cache is owned by VulkanTransformerModel and disposed
with the model. Lifetime constraint (host disposing the source adapter
before the model would leave a stale cache entry) is documented at the
class level — the simplest safe default for Phase 4b.

VulkanForwardState gains three lazy LoRA scratch buffers:
- LoraTmp        [seqLen, rank]      — first stage matmul output
- LoraDelta      [seqLen, outputDim] — second stage matmul output
- LoraDeltaSum   [seqLen, outputDim] — y + delta scratch (AddKernel can't
                                        alias its readonly A and writeonly C
                                        bindings to the same buffer)
Allocated lazily via EnsureLoraScratch so non-LoRA forwards pay zero
extra VRAM, and grown monotonically alongside the seqLen-driven main
scratch (dropped + recreated when EnsureCapacity reallocates so the
cached descriptor sets pointing at them are invalidated together).

Tests:
- VulkanLoraAdapterUploadTests.Upload_ScalesB_AndPreservesA — verifies
  alpha/rank is folded into B on upload while A round-trips verbatim.
- VulkanLoraAdapterUploadTests.Cache_ReturnsSameInstance_OnRepeatLookup
  — verifies the cache returns the same VulkanLoraAdapter instance on
  repeat GetOrAdd of the same source adapter.
- VulkanLoraAdapterUploadTests.Upload_StripsNonStandardProjections —
  out-of-scope (e.g. q_a_proj for MLA) names are skipped at upload so
  callers that mistakenly attach them don't blow up the upload path.
3/3 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e 4b)

Wires the Vulkan LoRA delta y += scale * (x · B) · A into
VulkanTransformerModel.Forward at every standard transformer projection
site (q/k/v/o + gate/up/down). MLA-attention and MoE-FFN sites are
deliberately out of scope (validation rejects adapters targeting them),
matching the CPU-side Phase 4a slice exactly.

VulkanTransformerModel gains:
- A 5-arg Forward(tokens, positions, deviceId, kvCache, adapter) overload
  mirroring the CPU TransformerModel signature. When adapter is null it
  short-circuits to the existing 4-arg overload (zero overhead, byte-for-
  byte identical to the pre-Phase-4b path). When non-null it validates
  shape compatibility, lazily uploads via VulkanLoraAdapterCache, sizes
  the LoRA scratch via VulkanForwardState.EnsureLoraScratch, sets
  _currentLora, and routes through the inner Forward with a try/finally
  to clear the field even on exception.
- ValidateAdapterForModel mirrors the CPU side: rejects MLA-attention
  projections and MoE-FFN projections with a NotSupportedException so
  the surface contract between CPU and Vulkan is identical.
- MaybeApplyLoraDelta dispatches the LoRA delta as four kernels (Option A
  per the Phase 4b brief — no new SPIR-V shaders this commit):
    1. tmp = matmul_f32(B, x)            // [seqLen, rank]
    2. delta = matmul_f32(A, tmp)        // [seqLen, outputDim]
    3. deltaSum = AddKernel(y, delta)    // y + delta into scratch
    4. vkCmdCopyBuffer(deltaSum -> y)    // land result back in y
  scale = alpha / rank is folded into B at upload time
  (VulkanLoraAdapter.Upload), so the matmul + add chain stays
  scale-agnostic. Three barriers (compute→compute, compute→transfer,
  transfer→compute) cover the inter-stage synchronisation.

Wire-up sites (mirrors the CPU ApplyLoraDelta call sites):
- Q/K/V projections: after the bias-add, before QK-norm / RoPE.
- O projection: after the o-bias, before the residual add #1.
- Gate/Up projections: after the gate/up biases, before SwiGLU.
- Down projection: after the down-bias, before the residual add #2.
The fused rmsnorm + matmul_q8_0 sub-tile kernel still writes F32 normOut
in its existing contract (see RmsNormMatmulQ8_0FusedKernel xmldocs), so
the LoRA delta's input is materialised in either branch — no fused-path
bypass is needed (the Vulkan fused kernel differs from the CPU's fused
RmsNormQuantize which writes only Q8_1).

Tests:
- VulkanLoraForwardParityTests.Forward_NoAdapter_VsZeroAdapter_AreIdentical
  — Vulkan with a zero-factor adapter is byte-equivalent to Vulkan
  without any adapter at abs 5e-3.
- Forward_NonZeroAdapter_ProducesMeasurableDelta — confirms the LoRA
  path is not silently disabled (maxAbsDiff > 1e-3 over the synthetic
  fixture).
- Forward_NonZeroAdapter_VulkanMatchesCpu — the load-bearing parity
  assertion: Vulkan-with-adapter matches CPU-with-same-adapter at
  abs 5e-3 / rel 1e-3 on the 32-vocab tiny synthetic Llama fixture.
- Forward_AdapterCache_AmortisesUploadCost — confirms both first and
  cached forward stay under the Phase 7 100 ms swap target.
4/4 pass.

Full Vulkan unit suite: 367/367 passed (1 skipped) — no regressions in
existing F32 / Q8_0 / Q4_K / Q5_K / Q6_K / F16 / BF16 tests when no
adapter is supplied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integration sister to the CPU TinyLlamaLoraAdapterTests: downloads the
same public llamafactory/tiny-random-Llama-3 base + tiny-random-Llama-3-lora
PEFT adapter (~8MB + ~27KB), loads each into both backends, runs forward
with the adapter on the same single-token input, and asserts the Vulkan
logits match the CPU oracle at abs 5e-3 / rel 1e-3 across all 128 256
vocab entries.

Self-skipping like the CPU sister test:
- Vulkan unavailability (no loader / driver / SPV blobs) → Skip.
- HF candidate unavailability (offline, rate limit, repo removed) → Skip.

Local run output:
  Adapter: rank=8 alpha=16 adapted_layer_count=14
  Vulkan Forward(adapter) took ~200 ms (first call — includes upload)
  Finite=128256/128256  maxAbs=8.94e-08  maxRel=0.0745  errors=0/128256

Note on seqLen=1: tiny-random-Llama-3 has hidden_size=16, num_kv_heads=4,
head_dim=4 — at seqLen>1 the F16 GEMM coopmat kernel rejects the K=16
contraction (it requires K % 32 == 0). This is a base Vulkan F16-coopmat
limitation orthogonal to LoRA; the decode path (seqLen=1) avoids it
cleanly and is the more important regime to pin for an adapter forward.
The synthetic-fixture parity tests in Phase-4b unit suite already cover
the seqLen>1 prefill path on a non-coopmat-tripping shape.

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

Threads per-request LoRA adapter selection through the OpenAI-compatible
chat-completions and raw-completions endpoints, exposes hot-load /
hot-unload / list admin endpoints behind a config flag, and registers
a process-wide LoraAdapterRegistry singleton on ServerState.

API additions (purely additive — no existing behaviour change):
- ChatCompletionRequest / CompletionRequest gain optional lora_adapter
  field. When unset, request runs against the base model exactly as
  before. When set, the registry resolves it to an ILoraAdapter and
  the runtime applies the LoRA delta during forward passes.
- POST /v1/lora/load { name, path } — register a HF PEFT adapter
  (gated by Server:AllowLoraAdminApi, default false → 403 Forbidden).
- DELETE /v1/lora/{name} — unload (same gating).
- GET /v1/lora — list registered adapter names (always available,
  read-only).

Engine changes:
- TextGenerator.Generate / GenerateStreamingTokensAsync /
  GenerateStreamingAsync gain optional ILoraAdapter? adapter parameter,
  passed through to IModel.Forward(.., kvCache, adapter) at every prefill
  + decode call site. Default null preserves byte-for-byte parity with
  pre-Phase-4c behaviour.

Wiring:
- LoraAdapterRegistry constructed in ServerStartup via the production
  PeftAdapterLoader factory (CreateLoraRegistry); attached to ServerState
  in both CreateBareState and LoadModel.
- ModelManagementEndpoint preserves the registry across model swaps —
  the new LoadModel() mints its own registry, but we discard the new one
  and keep the existing registry (so loaded adapters survive a swap).
- LoraEndpoints.Resolve() centralises 'name → adapter' lookup; on miss
  throws LoraAdapterNotFoundException whose message includes the list of
  currently-loaded adapters. Both endpoints catch it and respond 400 with
  the diagnostic message.
- ServerJsonContext registers the new DTOs (LoraLoadRequest /
  LoraLoadResponse / LoraListResponse) for AOT-safe serialisation.

Backward compat:
- All existing /v1/chat/completions and /v1/completions tests pass
  unchanged; lora_adapter is opt-in.
- Admin endpoints return 403 by default — operators must opt-in via
  AllowLoraAdminApi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Phase 4c group-then-serial multi-adapter batcher and the
matching unit tests for the new server admin/request surface.

MultiAdapterBatcher.Group<T> partitions a request batch by adapter
identity, yielding the base-model (null adapter) group first followed
by each distinct adapter in first-seen order. Within each group the
intra-batch request order is preserved. Reference equality on the
adapter handle is the partition key — the registry's "single instance
per name" contract guarantees this matches name equality without us
having to resort to string compares on the hot path. Pure /
allocation-light: empty batch returns Array.Empty; same-adapter batch
allocates one bucket; mixed batch allocates one Dictionary keyed by
ReferenceEqualityComparer.Instance.

LoraEndpointsTests covers:
- DTO deserialiser: lora_adapter is null when absent (backwards-compat
  for every existing /v1/chat/completions and /v1/completions test) and
  round-trips when present, on both ChatCompletionRequest and
  CompletionRequest.
- LoraEndpoints.Resolve: null/empty name → null adapter; unknown name →
  LoraAdapterNotFoundException whose message includes the list of
  currently-loaded adapter names ("none loaded" when registry is
  empty); known name → returns the registry's adapter handle.
- Registry round-trip: load + list + unload + duplicate-load rejection.
- Admin gating: AllowLoraAdminApi defaults to false.

MultiAdapterBatcherTests covers the partition contract — empty,
all-base, all-same-adapter, mixed (a / b / null interleaved), order
preservation, null-group-first invariant — plus a deliberately skipped
ConcurrentMixedAdapter_Note placeholder that documents the engine's
current SemaphoreSlim(1, 1) request gate as the reason true concurrent
mixed-adapter batching is deferred to Phase 4d / Wave 9. The skip
message points to MultiAdapterBatcher for the partition contract that
the future scheduler will plug into.

Suite: 1767 pass, 158 skip (Phase 4b baseline 1740/157 + 27 new
adapter / batcher / DTO assertions; 1 new skip for the future
concurrent-batching test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ch (Phase 4d)

Three independent stretches under one commit:

4d.1 — Quantised LoRA adapter weights
- LoraLayerWeights gains an optional WeightDType field (F32 default —
  byte-equivalent backward compat). Source dtype is preserved through to
  the runtime kernel.
- LoraDelta.Apply gains a dtype-aware void* overload that dequantises B
  and A into ArrayPool F32 scratch and reuses the F32 GEMM path. F32
  dispatch short-circuits to the original kernel.
- PeftAdapterLoader gains preserveSourceDType parameter; F16 / BF16
  tensors are stored verbatim and reuploaded as-is. Halves adapter
  memory for the typical PEFT case.
- Q8_0 LoRA deferred — most PEFT trainers ship F16; Q8_0 is niche.

4d.2 — MLA + MoE adapter acceptance
- TransformerModel.ValidateAdapterForModel no longer rejects standard
  q/k/v/o or gate/up/down adapters on MLA / MoE base models. The
  standard ApplyLoraDelta call sites are gated by lw.Mla / lw.Moe so
  non-applicable projections are silent no-ops at runtime, not
  load-time errors. Real MLA-LoRA / MoE-LoRA adapters require
  additional MLA-specific (q_a_proj/q_b_proj/...) and per-expert
  (mlp.experts.{j}.{...}) projection wiring at the call sites — tracked
  as a follow-up; no public PEFT release exists today.

4d.3 — Perf bench
- benchmarks/Lora/LoraDeltaOverheadBenchmark — kernel-level bench
  comparing baseline F32 GEMM to GEMM + F32 LoRA delta + GEMM + F16
  LoRA delta at TinyLlama q_proj shapes (hidden=2048, seq∈{1,128}, r=16).
  ShortRunJob so the bench finishes in <30s. Auto-discovered by the
  existing BenchmarkSwitcher in Program.cs.

Tests:
- LoraDeltaQuantizedDtypeTests — F16/BF16/F32 parity vs scalar
  reference (abs tol 5e-3 F16, 5e-2 BF16; F32 dispatch is bit-exact).
- LoraMlaMoeAcceptanceTests — adapters validate clean against MLA
  (DeepSeekV3) and MoE (Llama+MoeConfig) base configs; native-memory
  dispose path verified.
- All existing 1767+ unit tests + Vulkan integration tests unchanged.

Acceptance caveats / scope reductions documented in commit:
- 4d.2 is acceptance-only (validation lift). Wiring delta application
  at MlaAttention.Execute / MoeSwiGluMlp dispatch sites requires a
  real PEFT adapter to validate against and is deferred.
- 4d.3 is a kernel-level bench, not a full TinyLlama checkpoint
  forward. The 5% target for the kernel overhead is reproducible
  without checkpoint download; macro-bench follow-up tracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the prior handoff (which targeted the Mamba-3 SISO wiring
that landed long ago) with the current state:

- 45+ commits this session across Phase 1 (K-quants Q4_K/Q5_K/Q6_K
  Vulkan), Phase 2 (real-weight Vulkan parity, 9 tests across 6
  architectures + 3 GGUFs), Phase 3 (long-context YaRN reference +
  verification), Phase 4 (LoRA full stack — foundation/Vulkan/
  server-API/quant), Phase 8 (F16/BF16 native Vulkan, brought
  forward as Route C alternative), plus the CUDA bring-up rebase
  merge from a sibling machine.
- Vulkan unit suite 115 → 367. Branch and main both at 9864bc6.
- Coverage matrix per architecture (CPU/Vulkan/real-weight) and
  per quant format (F32/F16/BF16/Q8_0/Q4_K/Q5_K/Q6_K all native
  on Vulkan; Q2_K full L3 on CUDA via the merge).

Next batches:
- Batch 1 (parallel, light): Phase 6 (sliding-window real-weight
  test) + Phase 9 (ALiBi position encoding) + Phase 12 (cosmetic
  cleanup). Disjoint file scopes so safe for concurrent agents.
- Batch 2 (sequential, heavier): Phase 5 (real-weight downloads
  for Mistral / DeepSeek-V3 / Mixtral / Qwen-MoE / OLMoE) → Phase 7
  (Q8_0 MoE indexed-expert kernel) → Phase 10 (paged attention
  KV-cache, multi-commit milestone) → Phase 11 (coopmat MoE
  Strategy C, multi-week, possibly own sub-branch).

Plus three LoRA follow-ups (4d.2 actual delta wiring at MLA + MoE
expert sites, 4d.3 macro-bench against real TinyLlama, fused
RmsNormQuantizeAndKeepF32 to recover the LoRA-active Vulkan
fused-rmsnorm bypass cost) tracked as a small bundle to land
before Phase 10.

Preserves the CUDA-side carry-forward from the prior handoff:
Spec 1 Phases 2-5 (IQ4 / IQ3 / IQ2 / IQ1 kernels), V2-Lite Q4_K_M
decode bench, CUDA HF-parity gates, Q2_K real-GGUF smoke, larger-
model scaling. Adds a cross-backend follow-up note: Vulkan IQ-family
parity once CUDA Spec 1 lands them.

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

kkokosa commented Apr 28, 2026

Copy link
Copy Markdown
Owner

This PR becomes way too large... 568 files changed, 140k lines added. When do you plan to stop? 😇

@jamesburton

jamesburton commented Apr 28, 2026 via email

Copy link
Copy Markdown
Author

@jamesburton

jamesburton commented Apr 28, 2026 via email

Copy link
Copy Markdown
Author

@jamesburton
jamesburton marked this pull request as ready for review May 6, 2026 15:48
Copilot AI review requested due to automatic review settings May 6, 2026 15:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@jamesburton
jamesburton requested a review from Copilot May 6, 2026 15:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@jamesburton

Copy link
Copy Markdown
Author

This PR becomes way too large... 568 files changed, 140k lines added. When do you plan to stop? 😇

Do you need me to break this out into smaller chunks? Grabbing my branch and running the tests and benchmarks to confirm it might be quicker than managing a batch of sequential PRs extracted from this, but if it helps you get things merged then I can guide an agent to extract in parts as more targeted changes building through to this.

@jamesburton

Copy link
Copy Markdown
Author

Closing this in favour of the breakout you asked for. 👍

You flagged that this branch had grown too large to review (the 568-file / 140k-line state). Since then it's been decomposed into a series of small, self-contained PRs against main, each with its own tests. The Mamba-3 work specifically is now:

…and the wider scope this PoC had accreted (Vulkan scaffold in #160, plus the CUDA / wide-model / CPU-kernel pieces) is split across the rest of the currently-open series (#142#320). Keeping this monolith open alongside them just adds review noise, so I'm closing it as superseded. Happy to open a small Mamba-3 umbrella tracking issue if that'd help sequence the set. Thanks for the nudge to split it up.

@jamesburton jamesburton closed this Jun 9, 2026
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.

3 participants