Skip to content

feat(everything-bagel): clean migration of PR #9 + #10 onto updated dev#14

Merged
ekryski merged 54 commits into
devfrom
tom/bagel-clean
May 27, 2026
Merged

feat(everything-bagel): clean migration of PR #9 + #10 onto updated dev#14
ekryski merged 54 commits into
devfrom
tom/bagel-clean

Conversation

@TheTom

@TheTom TheTom commented May 26, 2026

Copy link
Copy Markdown
Contributor

Strategic re-port of PR #9 + PR #10 (Everything Bagel 1 + 2) onto the post-AURA-port dev. The original PRs stay open for posterity; this branch is the cleanly-commented, scope-audited replacement that targets the updated layout (Eric's Ops/ / KVCache/ / Loader/ / Generation/ split) and conforms to the repo's commit-hygiene + conventional-commit rules.

Measured perf

M5 Max / 128 GB (Apple9) — Qwen3.5-35B-A3B-4bit:

Shape Result
Decode T=1, empty KV 96.51 tps (median of 5 × 32 steps at 0.331–0.333 s) — matches PR #10 tip of 96.3 tps
Prefill T=128 369 tps batched, 3.81× over per-token loop
Prefill T=512 459.3 tps batched, 4.76× over per-token — matches PR #10 tip of 455 tps
Prefill T=2048 430.7 tps batched, 4.52×
Prefill T=32K 134.4 tps batched (single 243.8 s run)
Decode T=1 after T=32K prefill 14.45 tps over 32 steps (10-attn-layer KV-bandwidth bound)

M2 Pro / 32 GB (Apple8) — Qwen3.5-2B-4bit (dense GDN-hybrid, same engine class minus MoE-specific paths):

Shape Result
Decode T=1, empty KV 77.83 tps (median of 5 × 32 steps at 0.407–0.413 s)
Prefill T=128 328.2 tps batched, 4.21× over per-token
Prefill T=512 110.3 tps batched, 1.42× over per-token (M2 GPU saturates earlier than M5 Max)

Same engine class (m.qwen35) on both, so the bench exercises every wiring landed: GPU MoE router (decode + prefill — M5 Max only since M2's 32 GB can't load the 35 B-A3B MoE checkpoint), fused Q+K+V projection at decode + prefill, chunked GDN recurrence, fused conv1d+silu+cast, rmsNormQgemvInt4Fast finalNorm, ropePartialManyTwo, kvCacheUpdateKVMany, sdpaMulti d=256 (the silent-miscompute fix), sdpaDecode2Pass, MTLResidencySet weight pinning.

Depends on metaltile PRs

Six new fused kernels + two pre-existing kernel PRs need to merge upstream before consumers without our metaltile fork can build this branch. We regenerate locally from tom/feat/batched-qkv-qmm and ship the build artefacts via the standard Generated/ pipeline; no Swift change is required on this side once they land.

  • #193 mt_sigmoid_scalar_fma_residual — sigmoid-gated FMA with residual fold (post-MoE-FFN).
  • #194 ffai_sdpa_multi_tree_mask — tree-causal SDPA for spec-decode tree verify.
  • #196 ffai_sdpa_bidirectional d80 + d96 — DiNOv2 / Idefics3 / Phi-3.5-vision head-dim coverage.
  • #197 ffai_sdpa_multi_d256 — Qwen3.6-A3B full-attention layers (closes the silent-miscompute when routed through the d=128 kernel).
  • #198 batched-prefill family — rope_llama_many, kv_cache_update_many, conv1d_causal_step_silu_cast_many.
  • #199 batched-projection family — batched_qkv_qmm_fast, batched_4_qgemv_fast, batched_4_qmm_fast.
  • #200 gated_rms_norm_qgemv_int4_fast — GDN tail fusion.
  • #201 moe_down_swiglu_accum_int4_chain8 — 3-stage MoE per-expert path.

All 8 are CLEAN + MERGEABLE with green CI.

What's in this PR

Foundation. MTLResidencySet pin helper on Device, applied at Model.load + KVCache / GDNStateCache / ConvStateCache init so the driver stops re-validating per-allocation residency on the thousands of decode-step dispatches. defaultMaxCommandBufferCount bumped 16 → 64 so the Metal driver pipelines more cmd buffers in flight before applying backpressure.

Single-encoder / paired-dispatch Ops. scalarFMA + Many + Chain8, sigmoidScalarFMAResidual, dequantGemvInt4Two/Three/Four/Many, ropePartialTwo/Many/ManyTwo, gatherTwo, rmsNormRowsTwo, kvCacheUpdateKV/Many, siluCastF32PlusCastF32Two, castToF32Two/Three, swigluMany, gatedMixerNormMany, conv1dCausalStepSiluCastMany. Each pairs / batches N kernel calls onto one encoder, eliminating begin/end overhead at the rate we hit decode (thousands per token).

Fused-kernel wrappers. moeRouterTopK + Many, dequantGemvInt4ExpertIndexed + Many (the GPU MoE router pair — biggest decode T=1 lever), sdpaMultiTreeMask, sdpaDecode2Pass (with the blocks ≥ 32 and multiple of 32 precondition PR #10 silently violated), gatedDeltaPrepChunk, batchedQkvQgemvInt4Fast + batchedQkvQmmFast, batched4QgemvInt4Fast + batched4QmmFast, rmsNormQgemvInt4Fast, gatedRmsNormQgemvInt4Fast, moeDownSwigluAccumInt4Chain8.

Model wirings. Qwen35AttentionMixer.forward (batched QKV + 2-pass SDPA + scratch caching), Qwen35AttentionMixer.forwardMany (batched QKV qmm + sdpaMulti d=128/d=256 + ropePartialManyTwo + kvCacheUpdateKVMany), Qwen35GDNMixer.forward (batched 4-projection + fused silu+cast2 + residency pinning), Qwen35GDNMixer.forwardMany + forwardManyChunked (chunked recurrence + conv1dCausalStepSiluCastMany + gatedMixerNormMany), Qwen35Model.finalNorm+lmHead (fused rmsNormQgemvInt4Fast across decode / prefill / VLM embed-input paths), Qwen35DenseMLP + Qwen35MoEFFN shared expert (fused Ops.swiglu), Qwen35MoEFFN.forward (shared-encoder batching + sigmoidScalarFMAResidual), MoELayer.decode + MoELayer.decodeMany (GPU router fast path defaulted ON + bm64 default at mTotal ≥ 1024). Cross-family Ops.swiglu adoption in Granite4 / LFM2 / Jamba dense MLPs.

Speculative decode. Drafter protocol + NGramDrafter + NeverDrafter + NGramTreeDrafter + tree-causal mask + DraftTreeNode.verify (20 host-side unit tests). Qwen35Model.forwardManyAllLogits for per-position verify. GDNStateCache / ConvStateCache snapshot/restore + composite CacheSnapshot for rejection rollback. SpecDecode.generateGreedy — greedy spec-decode driver against any Drafter.

Audit findings worth calling out

sdpaMulti was a silent-miscompute on Qwen3.6-A3B full-attention layers — dev only switched on dtype and ran the d=128 kernel at head_dim=256. Restored the (dtype, headDim) tuple-match + relaxed OpsValidation.validateSdpaMulti; runtime test pins both paths.

addAndRmsNorm on dev supersedes PR #10's addRmsNorm + addRmsNormRows — same kernel, tuple-return signature, nRows-aware. rmsNormResidual is a distinct op (single-output fused norm) and is NOT equivalent to PR #10's addRmsNorm — verified by reading both kernel bodies.

sdpaDecode2Pass pass-2 kernel hardcodes a 32-lane block reduction (bn = 32, block_chunks = blocks / 32), so blocks MUST be a multiple of 32 and ≥ 32. PR #10's wrapper let blocks=2 through; output was silently zero. Wrapper precondition catches it now and the runtime test exercises blocks=32, nKV=64 against single-pass sdpaDecode.

QuantizedLinear.callMany's 4-bit fast path needs outDim % 32 == 0 (the BN tile of dequantGemmDynamicM). Without the guard, Qwen3.5's 4-bit shared_expert_gate at outDim=1 trips Ops.dequantGemmDynamicM: nOut (1) must be multiple of 32. Fall-through to the per-row dequantGemv loop is bit-identical at that shape.

Deferred per spec (not in this PR)

  • ITER 96 gatedRmsNormQgemvInt4Fast attention wiring — Op landed but wiring stays out; PR Everything Bagel 2 #10 documented -7% decode regression at out_dim=2048 (TG-mem staging caps occupancy below the unfused chain).
  • ITER 100 batched_4_qmm_fast GDN-input wiring — Op + wrapper landed but stays unwired; PR Everything Bagel 2 #10 documented workgroup imbalance at the Qwen3.6 GDN shape (out_c/d=16 ≪ out_a/b=2048 wastes 99% of TGs).
  • ITER 101 OpsICB (Day 2 plumbing) — 0.55× vs direct rebound at Qwen3.6's 40-layer MLP shape. ICB wins on many small non-dependent dispatches; not our workload.

Tests

Host-side unit tests for every new Op (CPU-truth or pair-equivalence in Tests/FFAITests/Ops/), 20 Drafter / tree-verify tests in Tests/FFAITests/Generation/, and integration benches in Tests/ModelIntegrationTests/Text/:

  • Qwen35MoEBenchIntegrationTests — decode T=1, T=128 / T=512 / T=2048 forwardMany, plus T=32K long-context (prefill + decode-after-prefill). Gated on local Qwen3.5-35B-A3B-4bit checkpoint.
  • Qwen36TextIntegrationTests.forwardManyBench{32,128,512,2048} — Eric's harness extended with T=2048 long-context case.
  • SpecDecodeBenchTests + SpecDecodeVerifyCostBench — greedy spec-decode parity vs baseline + verify-shape cost crossover.

Cross-hardware: all 47 new Op tests + 20 Drafter tests pass on both M5 Max (Apple9 / 128 GB) and M2 Pro (Apple8 / 32 GB). Build green on both. Decode + prefill perf measured on both — numbers in the table above.

Pre-existing dev test issues fixed along the way so CI goes green: PSOCacheTests.swift MTLComputePipelineState Sendable error under Xcode 16.4 (@preconcurrency import Metal); FireRedVAD.swift:1202 BINUNICODE8 length expression typing-out under strict concurrency (split into two 32-bit halves).

Known pre-existing dev test failure: Soprano-80M synthesize integration test hits KVCache: dtype mismatch precondition. Reproduces on waffles/dev tip with this branch's changes stashed, so unrelated to the migration.

Cross-reference

@TheTom
TheTom force-pushed the tom/bagel-clean branch from 6cf160f to b669cec Compare May 26, 2026 13:49
TheTom added 22 commits May 26, 2026 08:55
`Device.markWeightsResident(_:)` attaches the given MTLBuffers to a
persistent residency set on the command queue. Without it, the Metal
driver re-validates per-allocation residency on every command-buffer
encode; at large MoE models with thousands of dispatches per prefill,
that overhead dominates wall time.

The set is shared across all weight buffers and lazily created on
first call. Repeated calls add to it. macOS 15 / iOS 18 minimum;
older OSes no-op silently. `FFAI_NO_RESIDENCY_SET=1` disables for
A/B benching.

Per-model integration (call `markWeightsResident` after `Model.load`)
lands in a follow-up commit so this stays a pure infrastructure
add. KV / GDN / conv state cache pinning also lands separately so
the residency-set adoption can be diffed per surface.
Walks every entry of the loaded `SafeTensorsBundle` and registers its
backing MTLBuffer with `Device.markWeightsResident(_:)` immediately
after the bundle resolves, before `ModelRegistry.dispatchAndLoad` kicks
the per-family loader. The driver's per-command-buffer residency
re-validation is the cost we avoid.

Opt-out via `FFAI_NO_RESIDENCY_SET=1` (handled inside the helper).
Older OSes silently no-op via the `@available(macOS 15, iOS 18, *)`
gate on the helper.
Each cache class owns one or two MTLBuffers that persist for the
lifetime of a generation and get touched by every decode-step
dispatch. Without explicit residency pinning, Apple's Metal driver
re-validates per-allocation residency on every command-buffer
encode — at decode rates of thousands of dispatches per token, the
per-dispatch overhead dominates wall time.

Reuses the device-wide MTLResidencySet introduced for weight
buffers (see `Device.markWeightsResident`). The set is single-flighted
under a lock and shared across all cache classes; the underlying
Metal API gracefully no-ops on macOS < 15 and is gated behind
`FFAI_NO_RESIDENCY_SET=1` for A/B comparison.

- `KVCache.init` pins kBuffer + vBuffer
- `GDNStateCache.init` pins current + next recurrent-state buffers
- `ConvStateCache.init` pins the rolling-window state

Migrated from PR #9 (Everything Bagel 1) — measured win was part of
the 16.29 → 55.94 tps decode T=1 jump on Qwen3.6-A3B / M5 Max.
The Qwen3-MoE decode path needs a scalar-broadcast FMA primitive to
accumulate the top-K expert outputs into a single hidden-state
vector. Without it the path was forced into a
`Tensor.filled([hidden], weight)` host alloc per expert plus a
`mul + add` pair per expert — a host roundtrip and 2 dispatches the
GPU could absorb into one.

Adds four wrappers (all in `Ops/OpsFused.swift`) over kernels that
metaltile already emits:

- `scalarFMA(scalar, value, base, into:)` — single-dispatch
  `out[i] = base[i] + scalar[0] * value[i]`.
- `scalarFMAMany(scalars, values, acc:)` — N sequential dispatches
  reusing one compute encoder; Metal guarantees in-order encoder
  execution so the accumulation is safe.
- `scalarFMAChain8(scalars, values, out:)` — exact 8-way fused
  dispatch (one kernel reads 16 inputs at once), collapses the
  topK=8 expert accumulator into a single dispatch.
- `sigmoidScalarFMAResidual(gate, value, base, residual, into:)` —
  sigmoid-gated FMA with the post-MoE residual add folded in;
  Eric's `sigmoidScalarFMA` covers the non-residual variant.

Tests in `Tests/FFAITests/Ops/OpsSpecialPathTests.swift` cover each
wrapper with a closed-form expected value (4 new tests).

Migrated from PR #9 / #10 (Everything Bagel 1 + 2) — measured as
load-bearing for the decode T=1 16.29 → 96.3 tps win on
Qwen3.6-A3B / M5 Max.
Three batched wrappers around the existing `dequant_gemv_int4_*`
metaltile kernels that drive N back-to-back dispatches inside a
single compute encoder, sharing the `input` binding and the
`in_dim` / `group_size` constants. Saves N-1 encoder begin/end
pairs per call.

- `dequantGemvInt4Two`  — per-expert MoE gate + up projections.
- `dequantGemvInt4Three` — Qwen3-style attention q/k/v projections
  in the unfused decode path.
- `dequantGemvInt4Four`  — Qwen3.5 GDN mixer's qkv/z/b/a
  input-projection quartet, all reading the same xNorm.

CPU-correctness tests in `Tests/FFAITests/Ops/QuantizedOpsTests.swift`
reuse a `makeInt4Projection` helper to build packed weights +
scales + biases + expected GEMV output, then verify each batched
wrapper matches CPU truth for every output tensor.

Migrated from PR #9 / #10.
Both Q and K go through the same partial RoPE rotation with identical
`(headDim, rotaryDim, position, thetaBase, scaling)` parameters
every decode step on Qwen3-class models. `ropePartialTwo` sets the
RoPE constants once and dispatches both rotations through a single
shared compute encoder, saving one encoder begin/end pair per
attention layer per token.

The kernel is unchanged (`ffai_rope_llama_*`); the wrapper is a
buffer-binding rotation around the existing in-place rotation. A
test verifies bit-identical output against two sequential
`ropePartial` calls on the same inputs.

Migrated from PR #10.
Pairs two `ffai_gather_*` dispatches against the same `table`
binding inside one compute encoder, saving an encoder begin/end
pair when the caller needs to fetch two parallel id streams from
the same embedding (e.g. the Qwen3 attention head-half split path).

Migrated from PR #10.
Two `mt_rms_norm_*` dispatches inside one compute encoder, used by
the Qwen3 attention mixer's pre-RoPE Q-norm + K-norm pair: both
projections run the fast path (TPG = rowSize / 4) and have no data
dependency between them. The wrapper shares a single eps buffer
allocation when the two eps values are equal — the common case
since both norms come from the same checkpoint config.

The wrapper restricts callers to the fast `mt_rms_norm_*` path
(rowSize ≤ 4096 and a multiple of 128). Wider rows must fall back
to two separate `rmsNormRows` calls so the wide kernel is picked
per call.

Test verifies bit-equivalence against two sequential `rmsNormRows`
calls on the same inputs.

Migrated from PR #10.
Wraps two `kv_cache_update_*` dispatches in a single compute
encoder. Cache layouts, dtypes, and writing thread are identical
to `kvCacheUpdate`; this only saves the encoder begin/end pair
between the K-cache write and the V-cache write. With 10 full
attention layers on Qwen3.6-A3B, this drops 10 encoder pairs per
decode token.

Migrated from PR #10.
…n one encoder

Used inside the Qwen3.5 GDN T-loop where, every token, one tensor
goes through `siluCastToF32` (the gate input) while the two raw
gate scalars `aRaw` / `bRaw` go through plain `castToF32`. The
wrapper sets the silu PSO once, dispatches the silu, then switches
to the plain-cast PSO and dispatches the two raw casts — all
inside one compute encoder. Saves the encoder begin/end pairs the
sequential 3-call path would create per GDN layer per token.

Migrated from PR #10.
The Qwen3-MoE decode path historically had to commit + wait on a
single GPU layer just to read the router logits back to the host,
run a CPU top-K + softmax, and then dispatch the per-expert gate +
up + down GEMVs. Two new wrappers move both halves to the GPU:

- `moeRouterTopK` / `moeRouterTopKMany` — wrap the
  `mt_moe_router_topk_*` kernel. Single-row form takes
  `logits: [nExperts]` and writes `[k]` u32 indices plus `[k]`
  weights; the T-batched form iterates `program_id<0>()` across
  prefill rows in one dispatch.
- `dequantGemvInt4ExpertIndexed` /
  `dequantGemvInt4ExpertIndexedMany` — wrap
  `dequant_gemv_int4_expert_indexed_*`, which reads
  `expertIndex[0]` at kernel entry to pick which slab of a
  stacked `[nExperts, outDim, inDim/8]` u32 weight tensor to
  dequantize. The `Many` variant runs N sequential dispatches
  inside one compute encoder sharing the constexpr binding so
  Qwen3.6-A3B's topK=8 gate + up phase collapses to one encoder
  per MoE layer.

The router output indices buffer can be aliased into N single-u32
views (`buffer + offset = slot · 4`) and threaded straight into
the expert-indexed GEMV — no host readback per layer.

Tests:
- `dequantGemvInt4ExpertIndexedCorrectness` — stacks 4 experts'
  weights, picks expert 2, and checks the output matches a
  CPU-truth dequant + GEMV of that expert's slab.
- `moeRouterTopKDeterministic` — top-K of (1, 5, 3, 4) at k=2
  picks indices {1, 3}; weights sum to 1.0 and the larger logit
  dominates.

Migrated from PR #10 ITERs 54–60.
Eric's `sdpaMulti` on dev only switches on dtype — every call lands
on `ffai_sdpa_multi_*` (the head_dim=128-only kernel). Qwen3.6-A3B's
full-attention layers run at head_dim=256, and metaltile emits a
dedicated `ffai_sdpa_multi_d256_*` family with a 2-phase output
reduction that keeps the per-row threadgroup memory under Apple's
32 KB cap. Without the d256 routing the d128 kernel reads past
each row of K/V and produces silent garbage.

Switches `sdpaMulti` to a `(dtype, headDim)` tuple match so f32 /
f16 / bf16 each pick the correct kernel; relaxes
`OpsValidation.validateSdpaMulti` to accept `headDim ∈ {128, 256}`
and updates the matching validator test.

Adds a runtime test that mirrors the existing d=128 uniform-K case
at head_dim=256, exercising the new routing end-to-end.

Migrated from PR #10 ITER 93 (Qwen3.6-A3B prefill wiring needs it).
Eric's dev has the base versions (`castToF32`, `swiglu`,
`gatedMixerNorm`) but not the batched variants our PR9/10 wirings
rely on. Each variant reuses the same metaltile kernel and
collapses N independent dispatches onto a single compute encoder:

- `castToF32Two` / `castToF32Three` — Qwen3 GDN fused-prep path
  promotes 2-3 same-dtype tensors (convAct / aRaw / bRaw) to fp32
  every token; one shared encoder replaces 2-3 separate ones.
- `swigluMany` — MoE per-expert SwiGLU at decode runs topK
  independent dispatches per layer; one shared encoder per call.
- `gatedMixerNormMany` — Qwen3.5 GDN mixer's per-token norm is
  evaluated `T·Hv` times during prefill; widening the grid X axis
  from `Hv` to `T·Hv` collapses the T-loop into a single dispatch.

Tests in `Tests/FFAITests/Ops/OpsSpecialPathTests.swift` verify
each batched form matches the corresponding sequential reference.

Migrated from PR #10 (closes the Tier 2 paired-on-encoder gap
identified during the Eric-overlap audit).
…ateKVMany, conv1dCausalStepSiluCastMany, gatedDeltaPrepChunk

Eric's dev only has the single-token forms of these ops; the
PR9/10 forwardMany / decodeMany prefill paths replace the per-token
T-loop with a single batched dispatch.

- `ropePartialMany` — single dispatch covering T rows of `qk`
  rotated by per-row `positions[T]`; saves T-1 encoder pairs and
  matches T sequential `ropePartial` calls bit-identically.
- `ropePartialManyTwo` — Q + K share `positions` and constants on
  one encoder; row strides differ.
- `kvCacheUpdateKVMany` — writes T tokens' K + V rows in one
  encoder, indexed by `positions[T]`.
- `conv1dCausalStepSiluCastMany` — `T` depthwise conv + SiLU +
  fp32 cast collapsed into one dispatch with the conv state held
  in per-channel registers across the sweep.
- `gatedDeltaPrepChunk` — chunked-prefill counterpart to
  `gatedDeltaPrepStep`; runs the fused GDN prep + recurrence over
  a token range in one dispatch, keeping per-head recurrent state
  in registers across the chunk. `tLen` arrives as a `[1]` u32 GPU
  buffer so chunked dispatch chains avoid CPU readback.

All kernels (`ffai_rope_llama_many_*`,
`kv_cache_update_many_*`,
`ffai_conv1d_causal_step_silu_cast_many_*`,
`mt_gated_delta_prep_chunk_*`) are already emitted on the dev
metaltile.

Tests:
- `ropePartialManyMatchesSequential` — T=3 batched matches three
  sequential `ropePartial` outputs bit-identically.
- `kvCacheUpdateKVManyF32` — verifies multi-position K + V writes
  land in the right cache rows and leave untouched slots zero.

Migrated from PR #10 (ITERs 86–99).
…coder

Mirror of `dequantGemvInt4Two/Three/Four` for the per-expert MoE
down phase, where each chosen expert has its own activation tensor
but all N projections share PSO, `inDim`, and `groupSize`. The
wrapper sets the constexpr binding once and rotates
`(weight, scales, biases, input, output)` per dispatch, saving
N-1 encoder begin/end pairs versus N independent
`dequantGemvInt4` calls.

Test verifies the batched form matches three sequential
`dequantGemvInt4` CPU-truth references across distinct
(scale, bias) projections + distinct inputs.

Migrated from PR #10.
…code

Same dispatch contract as `sdpaMulti(causal: false)`, but the
scalar `causal` flag is replaced by an additive `[nQuery, nQuery]`
mask tensor consulted only for in-block KV positions. Positions
`< baseKV` (the cached prefix) are always attended. Mask is `0.0`
to allow and `-inf` to block, added directly to pre-softmax
scores.

Used by speculative-decode tree-verify: one verifier call attends
every leaf-to-root path of the draft tree at once, gated by a
tree-causal mask the drafter produces from its node structure.
Wraps `ffai_sdpa_multi_tree_mask_*` (headDim=128 only — no d256
variant for tree-verify on dev).

Tests:
- All-zero mask reproduces `sdpaMulti(causal: false)` output
  bit-equivalent (sanity check on the mask add path).
- Diagonal-only -inf mask isolates each query to its own KV slot:
  with uniform Q/K and per-row distinct V, output collapses to a
  copy of the matching V row.

Migrated from PR #10 ITER 70.
Pass 1 partitions the KV cache into `blocks` even tiles and
writes per-block partial `(O, max, lse)` triples; pass 2 merges
them with the log-sum-exp trick. Total work matches single-pass
`sdpaDecode` but the two passes parallelize across more GPU
threadgroups, paying off at long KV (≥ 1K typical) where the
single-pass kernel starves the GPU with one TG per Q-head.

Block-count contract (the load-bearing detail PR10's port
silently violated): the pass2 kernel hardcodes a 32-lane block
reduction (`bn = 32`, `block_chunks = blocks / bn`), so `blocks`
MUST be a multiple of 32 AND at least 32. Anything smaller makes
`block_chunks` round to zero — pass2's outer loop iterates zero
times and the output stays whatever pass1 left in scratch,
typically all zeros. The previous port's "power of 2" check let
`blocks=2` through and was the cause of the all-zero smoke
failures during the earlier port attempt; the new precondition
catches it at the wrapper.

Scratch buffers (`partialO`, `partialM`, `partialL`) are
caller-owned so a decode loop can amortise them across steps.

Test: `blocks=32, nKV=64, nQHeads=2` — verifies the two-pass
output matches single-pass `sdpaDecode` to within 1e-2 absolute.

Migrated from PR #10 ITER 53.
…jection)

Wraps `ffai_batched_qkv_qgemv_fast` (M=1 decode) and
`ffai_batched_qkv_qmm_fast` (M>1 batched prefill). Both fuse the
three Q / K / V int4 dequant-projection dispatches into ONE kernel
launch — `program_id<2>()` selects the matrix across a depth-3
grid, so all three projections share TG memory and read the input
activation once instead of three times from DRAM.

Decode path (M=1): replaces the 3-dispatch `dequantGemvInt4Three`
shared-encoder form. Saves 2 encoder begin/end pairs per attention
layer.

Prefill path (M>1): outputs land in three separate contiguous
tensors (`qBuf [M, out_q]`, `kBuf [M, out_k]`, `vBuf [M, out_v]`)
so downstream code reads each projection as `[M, dim]` without
strided views. Pre-zeroes the outputs since the kernel's tile
mechanic relies on smaller-matrix tile-past-out_* slots being
already-zero.

Kernel constraints (preconditioned): `in_dim % 512 == 0`, each
`out_q / out_k / out_v % 8 == 0`, `group_size = 64`, TPG = 64.
Dispatches f32 / f16 / bf16 variants.
…GDN input proj)

Wraps `ffai_batched_4_qgemv_fast` (M=1 decode) and
`ffai_batched_4_qmm_fast` (M>1 batched prefill). Extends the QKV
(3-output) pattern to 4 projections sharing the same input — used
by the Qwen3.5 GDN mixer where qkv / z / b / a all project from
the same `xNorm` and can fuse into a single kernel launch.

Decode: replaces the 4-dispatch `dequantGemvInt4Four` shared-
encoder form. Pays the input DRAM roundtrip once instead of four
times; saves 3 encoder begin/end pairs per GDN layer.

Prefill (M>1): reads `x = [M, in_dim]` once into TG memory and
produces all 4 outputs (each `[M, out_*]`) in one dispatch.
Eliminates the 3 redundant `xNormFlat` DRAM reads of the unfused
`callMany` chain.

Kernel constraints (preconditioned): `in_dim % 512 == 0`, each
`out_a/b/c/d % 8 == 0`, `group_size = 64`, TPG = 64. Dispatches
f32 / f16 / bf16 variants.
…+gemv)

Wraps `ffai_rms_norm_qgemv_fast` (RMSNorm-fused) and
`ffai_gated_rms_norm_qgemv_int4_fast` (Mamba2-style gated-RMSNorm-
fused). Both keep the normalised activation in registers and feed
it straight into the int4 dequant-GEMV, replacing a 2-dispatch
chain (`Ops.rmsNorm` / `Ops.gatedRmsNorm` + `Ops.dequantGemvInt4`)
with a single kernel. Saves a full `[in_dim]` DRAM roundtrip on
the intermediate `normed` per call.

`rmsNormQgemvInt4Fast` is the workhorse for the finalNorm + lmHead
boundary (one dispatch per token) and any other pre-norm → int4-qmm
pattern in transformer blocks.

`gatedRmsNormQgemvInt4Fast` is purpose-built for the Qwen3.5 GDN
mixer post-recurrence boundary: collapses
`silu(z) · w · rmsnorm(y)` + outProj qgemv into one launch.

Kernel constraints (preconditioned): `in_dim % 512 == 0`,
`out_dim % 8 == 0`, `group_size = 64`, TPG = 64. The gated variant
additionally requires `Hv·Dv ≤ 8192` (kernel stages the full input
in TG memory). Dispatches f32 / f16 / bf16 variants.
Wraps `ffai_moe_down_swiglu_accum_int4_chain8`. Collapses the
GPU-router MoE path's three back-to-back per-layer dispatches:

  1. `swigluMany`           — inner[k]  = silu(gate[k]) * up[k]
  2. `dequantGemvInt4ExpertIndexedMany` (down)
                            — out[k]    = W_down[expert_idx[k]] · inner[k]
  3. `scalarFMAChain8`      — acc[i]    = Σ_k slot_weight[k] · out[k][i]

into ONE kernel launch. Each threadgroup owns one output row of
`[out_dim]`, iterates the 8 slots sequentially: stages `inner` in
3 KiB threadgroup memory, runs the dequant-gemv inner-product
against `W_down[expert_idx[k]]` for that slot, accumulates into a
per-thread `acc` with the slot scalar baked in, then reduce-sums
across threads at the end. Eliminates the `inner[k]` and `out[k]`
DRAM roundtrips between phases — 16 buffer-write-then-read pairs
per layer at topK=8.

Kernel constraints (preconditioned): `in_dim ≤ 768` (TG-memory
cap, kernel statically allocates 768 floats), `group_size = 64`,
TPG = 128. Lives in OpsFused.swift next to `scalarFMAChain8`
(the third stage it collapses). Dispatches f32 / f16 / bf16
variants.

Note: kernel ships in metaltile PRs #196–#201 (regenerated
locally into Sources/MetalTileSwift/Generated/MetalTileKernels.swift
for this branch).
Batched-T KV append wrapper around Ops.kvCacheUpdateKVMany — replaces
the T-loop of Ops.kvCacheUpdateKV calls with one shared encoder + 2
dispatches. Plus the host-side slot reservation helper that fills a
u32 positions tensor under lengthLock. Pre-req for ITER 98 wiring
into Qwen35AttentionMixer.forwardMany.
@TheTom
TheTom force-pushed the tom/bagel-clean branch from b669cec to acd2733 Compare May 26, 2026 13:55
@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown

Commit message hygiene check

All commit messages and PR text are clean. ✅

TheTom added 5 commits May 26, 2026 09:00
… scratches

PR10 ITERs ported into the single-token attention decode path:

- ITER 22/28: cache sliceHeadHalves35 row-index tensors at init,
  pin to residency set (was rebuilt + memcpy'd per token).
- ITER 24: batch q+k+v projections through Ops.dequantGemvInt4Three
  when all 3 projections are QuantizedLinear int4 with matching groupSize.
- ITER 78: single-dispatch Ops.batchedQkvQgemvInt4Fast when constraints
  match (in_dim%512==0, out_*%8==0, groupSize==64). Backing scratch is
  one contiguous [qDim+kDim+vDim] buffer; gate via FFAI_NO_FUSED_QKV.
- ITER 65: Ops.gatherTwo replaces the two separate sliceHeadHalves35
  gathers with one shared-encoder dispatch.
- ITER 12/35: Ops.rmsNormRowsTwo collapses q_norm + k_norm into one
  shared encoder, cached scratch outputs.
- ITER 21: Ops.ropePartialTwo pairs Q+K RoPE on one shared encoder.
- ITER 53: Ops.sdpaDecode2Pass (Flash-Decoding) kicks in at long KV
  (default threshold 1024, FFAI_SDPA_2PASS_THRESHOLD overrides).
- ITER 11/40/43: Ops.sigmoidMul with cached output scratch replaces
  the mul+sigmoid chain at the attention output gate.
…ding

Speculative-decode candidate-proposer interface used by the
in-progress `SpecDecode` driver. Three implementations land here:

- `NGramDrafter` — prompt-lookup n-gram. Scans `history` for the
  last `nMatch` tokens, falls back to shorter n-grams down to
  `minNMatch`, returns up to `gamma` follow-on tokens from the
  earlier occurrence. Zero ML cost; the workhorse for code and
  structured-chat workloads where prior context recurs.
- `NeverDrafter` — stub that always returns `[]`. Used to bench
  pure spec-decode driver overhead vs raw single-token decode.
- `NGramTreeDrafter` — real branching tree drafter. Conforms to
  both `Drafter` (linear γ fallback via `propose`) and
  `TreeDrafter` (`proposeTree` returning top-K continuations per
  depth) for tree-verify spec decode against
  `Ops.sdpaMultiTreeMask`.

`DraftTreeNode` is the pure-Swift tree representation, with
`flatten()`, `verify(oracleAtHistoryEnd:oracle:)`, and
`treeCausalMask()` helpers — all pure functions, no model / cache
dependencies, ready to wire into the SpecDecode driver once
`forwardManyAllLogits` lands.

`LinearTreeAdapter` lets any linear `Drafter` masquerade as a
`TreeDrafter` (degenerate single-branch tree) so the tree-verify
driver can A/B against the linear baseline using the same
`NGramDrafter` instance.

Tests in `Tests/FFAITests/Generation/DrafterTests.swift` cover
all three drafters end-to-end: 20 host-side tests, no GPU
dependencies. All pass.

Migrated from PR #10 (ITERs 61, 68, 69, 71).
…many-RoPE/KV

PR10 ITERs ported into the T-batched prefill path:

- ITER 88: Ops.batchedQkvQmmFast fuses Q/K/V projections into one
  dispatch when fusedQKVEligible (same constraints as ITER 78
  single-token path).
- ITER 74: sliceHeadHalvesManyBoth35 helper — one shared-encoder
  Ops.gatherTwo dispatch for the queries+gate split (was two
  back-to-back sliceHeadHalvesMany35 calls).
- ITER 83: Ops.rmsNormRowsTwo collapses Q/K rmsNormRows into one
  shared encoder over (T·nHeads) + (T·nKVHeads) rows.
- ITER 84+97: Ops.ropePartialManyTwo replaces the T-loop of per-row
  ropePartial calls with ONE shared-encoder dispatch over all T
  tokens (Q+K paired). Saves ~5100 dispatches per layer at T=512.
- ITER 98: kv.appendRangeOnGPUMany + reserveSlotsManyOnHost — one
  shared encoder + 2 dispatches for the K/V cache append over all T
  tokens (was a T-loop of per-token kvCacheUpdateKV calls).
- ITER 90/93: Ops.sdpaMulti collapses T-token causal attention into
  ONE dispatch. headDim=128 routes to ffai_sdpa_multi; headDim=256
  (Qwen3.6-A3B) routes to ffai_sdpa_multi_d256. Other head_dims fall
  back to the T-loop sdpaDecode form.
- ITER 14: Ops.sigmoidMul on the prefill output-gate path.
ITER 16/17 (PR10): replace silu+mul chain with Ops.swiglu in
Qwen35DenseMLP.forward/forwardMany and Qwen35MoEFFN.forward (single-
token) + forwardMany (batched) shared-expert paths. One fused dispatch
per call vs two (silu + mul).
ITER 76/77/79 (PR10): collapse the finalNorm + lmHead 2-dispatch chain
into one Ops.rmsNormQgemvInt4Fast call when the lmHead is 4-bit
QuantizedLinear with constraints (in_dim%512==0, out_dim%8==0,
groupSize=64). Fires on Qwen3.6-A3B's quantized lmHead.

Wired in three forward paths via shared `qwen35FinalNormLmHead`
helper:
- Qwen35Model.forward (single-token decode) — ITER 76
- Qwen35Model._forwardManyBatched (batched-T prefill last-row) — 77
- Qwen35Model.forward(inputEmbedding:) (VLM embed-input) — ITER 79

Fallback to separate finalNorm + lmHead dispatches when constraints
not met (dense lmHead, int8/int3 quant, etc.).
TheTom added 10 commits May 26, 2026 09:26
Greedy spec-decode loop for Qwen3.5/3.6:
  1. Drafter proposes up to γ candidates via Drafter.propose.
  2. Snapshot caches; run forwardManyAllLogits([prev, c_0..c_{γ-1}])
     to advance caches by γ+1 tokens and get per-position logits.
  3. Greedy verify: accept consecutive argmax matches up to the first
     mismatch.
  4. On partial accept (any mismatch): restoreAll caches from
     snapshot, replay accepted prefix via single-step forward, take
     next-prev from the LAST single-step (bit-identical to baseline
     greedy loop; forwardManyAllLogits drifts slightly due to
     different kernel paths). On full accept: commit all candidates +
     bonus token from trailing logits[γ].
  5. Empty proposal → fall back to single-token forward.

Stops on maxNewTokens or stop-tokens. SpecDecodeStats records
acceptanceRate, tps, fallbackSingleSteps, etc.

Greedy-only for v0 — temperature / nucleus / top-K follows the same
loop with probability-ratio rejection comparing sampled tokens against
the drafter's proposed tokens.
…eTests

The concurrency test uses a TaskGroup of MTLComputePipelineState, which
under Swift 6 strict concurrency tripped 'type does not conform to the
Sendable protocol' on the CI runner (Xcode 16.4 SDK).

Adding @preconcurrency to the Metal import keeps the same test
behaviour but lets the compiler treat Metal's pre-Sendable protocols
as warnings rather than errors, matching the convention the rest of
the project uses for system-framework imports under strict
concurrency.
…en3.6

Integration test from PR #9 that compares baseline greedy decode
(forward loop) vs SpecDecode.generateGreedy + NGramDrafter at
gamma in 1, 2, 4. Verifies generated sequences match baseline
exactly (greedy + greedy drafter + correct snapshot/restore is
deterministic) and reports decode-only tps so the cost of the
verify forward is visible.

Gated on the local Qwen3.6-A3B checkpoint at
/Users/tom/models/Qwen3.6-35B-A3B-4bit; skips with a print when
the path is missing.
…ingle-step loop

Measures whether spec-decode's verify step should use
forwardManyAllLogits(T) or a loop of single forward() calls.

Times 16 iterations of each path with cache snapshot/restore between
iterations (so each iter starts in identical state) and reports the
median ms / iter ratio for T=3 (gamma=2 verify) and T=2 (gamma=1
verify) shapes. The answer determines the SpecDecode driver's
optimal verify primitive once the cost crossover point is found.

Gated on the local Qwen3.6-A3B checkpoint at
/Users/tom/models/Qwen3.6-35B-A3B-4bit; skips with a print when the
path is missing.
… type-check it

CI build was failing on Swift 6 / Xcode 16.4 with 'the compiler is
unable to type-check this expression in reasonable time' on the
8-byte little-endian length decode at line 1202. The pre-existing
single expression chained 8 byte loads + 7 bit-shifts + 7 bit-ORs
through Int promotion, which trips the type-inference budget under
strict concurrency.

Split into two 32-bit halves (nLo32 + nHi32) joined by one final OR;
identical bit semantics, type-check finishes in &lt;1 ms.
…harness

Same shape as Qwen3.6's forwardManyBench but pointed at the locally-
available Qwen3.5-35B-A3B-4bit checkpoint. Engine is m.qwen35 in
both cases, so this exercises every Bagel-migration wiring landed
on tom/bagel-clean (GPU MoE router, batched QKV, GDN chunked
recurrence, rmsNormQgemvInt4Fast finalNorm, 2-pass Flash-Decoding,
MTLResidencySet weight pinning).

Each test gates on the local checkpoint existing and prints a skip
message when missing.

M5 Max decode T=1 measurement: 96.87 tps (median of 5 × 32-step
runs), matching the PR #10 Qwen3.6-A3B tip target of 96.3 tps.
…32 guard

QuantizedLinear.callMany routes 4-bit weights to Ops.dequantGemmDynamicM
(mt_qmm_mma backed), whose kernel requires outDim to be a multiple of
32 (the BN tile of the MMA kernel). Without an outDim guard the
precondition tripped on Qwen3.5-35B-A3B's shared_expert_gate, which
quantises to 4-bit at outDim=1 (a per-token gate scalar):
'Ops.dequantGemmDynamicM: nOut (1) must be multiple of 32 (BN tile)'.

Add the multiple-of-32 check next to the 4-bit branch. Anything that
doesn't fit (4-bit narrow projections + every non-4-bit) falls through
to the per-row dequantGemv loop, bit-identical to the per-token path
and irrelevant to perf at outDim=1.

Verified on M5 Max with Qwen3.5-35B-A3B-4bit: forwardManyBench T=8 /
T=128 / T=512 all complete cleanly. Headline perf: decode T=1 96.87
tps, prefill T=512 458.6 tps (4.75x speedup over per-token loop) —
matches the PR #10 tip targets of 96.3 / 455 tps.
…e4/LFM2/Jamba dense MLP

Two ITERs from PR #10 the wirings agent missed in the first pass.

ITER 20: bump MetalTileLibrary.defaultMaxCommandBufferCount from 16
to 64. Lets the Metal driver pipeline more cmd-buffers in flight
before applying backpressure; absorbs per-cmd-buffer encode latency
across more concurrent submissions. PR #10 measured +2-3% prefill
T=512 on Qwen3.6-A3B / M5 Max. FFAI_MAX_COMMAND_BUFFERS override
preserved.

ITER 17: replace Ops.mul(Ops.silu(g, cmd), u, cmd) with the fused
Ops.swiglu in Granite4Text.MLP.forward, LFM2Text.MLP.forward, and
JambaText.MLP.forward. Same semantic; one kernel dispatch instead of
two, intermediate silu(g) stays in registers instead of round-
tripping to DRAM. Brings these three cross-family models in line
with Qwen3.5 (already wired via the agent's earlier ITER 16 commit
on Qwen35DenseMLP + MoEFFN shared expert).
… bench cases

Extends the bench harness with two long-context shapes that PR #10's
work needs to validate but Eric's existing harness only covered up to
T=512:

- forwardManyT2K (T=2048): exercises chunked GDN recurrence over a
  longer window + larger SDPA prefill grid.
- decodeAfterLongPrefill: prefill 1024 tokens via forwardMany, then
  measure decode-only tps over 32 steps. Catches any KV-cache /
  residency / sdpaDecode2Pass routing regression at non-trivial KV.

Measured on M5 Max, Qwen3.5-35B-A3B-4bit:

  forwardManyBench T=2048: per_token=21499ms batched=4755ms
                           speedup=4.52x batched_tps=430.7

  decode T=1 after T=1024 prefill:
    runs=[0.335, 0.335, 0.336, 0.336, 0.337]s median=0.336s -> 95.37 tps

Decode tps barely drops from empty-cache (96.51) to KV=1024 (95.37)
- the kernel-compute floor PR #10 documented at 96 tps holds through
the long-context regime.
…-context bench

Drops the lukewarm 1024-token case (KV size barely above empty-cache,
decode tps barely budged from 96.51 to 95.37) and replaces it with
a real long-context probe at T=32768.

Single-run shape — 5-run median is impractical at 32K (each run is
multi-minute). Per-token baseline is intentionally not measured at
that size (would be 5+ minutes per run). Measures:

  - Batched forwardMany at T=32768.
  - Decode-only tps over 32 steps after the T=32768 prefill, so any
    KV-cache scaling regression (sdpaDecode2Pass routing, residency
    pinning, etc.) surfaces.

Measured on M5 Max / Qwen3.5-35B-A3B-4bit (single run):
  Prefill T=32768: 243.829s -> 134.4 tps batched
  Decode T=1 after T=32768 prefill: 2.215s over 32 steps -> 14.45 tps

The decode drop from 96 tps (empty KV) to 14.45 tps (32K KV) is the
expected 10-attention-layer KV-bandwidth tax — 30 of 40 layers are
GDN with O(1) state, but the 10 full-attention layers must walk the
linearly-growing K/V caches every decode step.
@TheTom
TheTom marked this pull request as ready for review May 26, 2026 16:03
@TheTom
TheTom requested review from 0xClandestine and ekryski May 26, 2026 16:03
CI's macos-latest is on Xcode 16.4 which lacks the async `MTLCommand-
Buffer.completed()` extension Apple added in Xcode 26.x. Local dev box
on Xcode 26.5 ships `completed()` AND flags the long-standing sync
`waitUntilCompleted()` as "unavailable from asynchronous contexts" in
async test functions. So neither single API works on both toolchains.

Adds `MTLCommandBuffer.awaitCompletion()` to `Tests/Helpers/CommonTest-
Helpers.swift` — bridges `addCompletedHandler` through `withChecked-
Continuation`. Works on every Metal SDK from macOS 10.11 onward, no
compiler complaints on either toolchain, no real-thread blocking.

Replaces 11 call sites in:
- Tests/ModelIntegrationTests/Text/Qwen35MoEBenchIntegrationTests.swift
- Tests/ModelIntegrationTests/Text/Qwen36TextIntegrationTests.swift
- Tests/ModelIntegrationTests/Text/SpecDecodeVerifyCostBench.swift

Pre-existing CI break that surfaced after the metaltile dependency PRs
landed in #193-#201 unblocked compilation past the previous
"MetalTileKernels has no member ..." cascade.
@TheTom
TheTom force-pushed the tom/bagel-clean branch from 4b882b0 to 0983066 Compare May 26, 2026 21:44
@ekryski ekryski mentioned this pull request May 26, 2026
3 tasks
The mt_moe_gather_qmm_mma_int4_bm64_mpp_bf16 kernel uses MPP cooperative
tensor intrinsics (simdgroup_matrix on cooperative-tensor types) that
require Apple GPU Family 9 or later. On Family 7 (M1, CI runner) the
kernel compiles but emits zeroed output, producing cos=0.0 vs the bm16
and m1 references. The kernel is independently covered by upstream
metaltile's GPU correctness suite on adequate hardware.

Gates all 7 bf16 cells in MoEBgemmBm64MppTests with .enabled(if:) using
a static MTLDevice.supportsFamily(.apple9) probe. M5 Max (Family 9+)
runs untouched; M1 runners skip with a clear reason string.
@ekryski ekryski mentioned this pull request May 27, 2026
ekryski added 4 commits May 26, 2026 23:34
- Drop `let _ = resultFlat` immediately before `return resultFlat` in
  Qwen35DecoderLayer.decodeMany; it was a stale leftover.
- Drop unused `let dt = hFlat.dtype` + `let dtBytes = dt.byteSize` pair
  in both Qwen35GDNLayer.decodeMany and Qwen35AttentionLayer.decodeMany.
The batchedQkvQ*Fast and batched4*Fast wrappers derive `in_dim` from
`wQ.shape[1] * 8` (or `wA.shape[1] * 8` for the 4-way) and pass that
single value to the kernel. The kernel walks every weight tensor at
that packed width, so a mis-packed wK/wV (or wB/wC/wD) would silently
miscompute its branch's output rather than fail at dispatch time.

Add an explicit precondition that all packed widths match the leading
weight tensor, with an error message naming the mismatched widths so
the failure mode is obvious if it ever fires.

Affects:
- Ops.batchedQkvQgemvInt4Fast
- Ops.batchedQkvQmmFast
- Ops.batched4QgemvInt4Fast
- Ops.batched4QmmFast
MoELayer and Qwen35{AttentionMixer,GDNMixer,MoEFFN} each hold a growing
set of per-instance scratch tensors that are mutated by `decode` /
`forward` / `forwardMany` without internal synchronisation. The
implicit contract — one inference per layer instance at a time — is
satisfied today by the per-token serial model loop, but is not
expressed in the API.

Make it explicit on every affected class header. Also tighten the
contract note on `MoELayer.decodeGPURouter` to call out that the
returned tensor aliases a scratch the GPU is still writing to via the
just-committed cmd, so a re-entrant call would race the in-flight
work.
GDNStateCache.snapshot() and ConvStateCache.snapshot() both return a
single per-instance scratch tensor that is overwritten on subsequent
calls. The current spec-decode driver does snapshot → verify → restore
once per layer per verify step so this is safe in practice, but the
contract is invisible to a caller reading just the method signature.

Spell it out: a second snapshot() before the matching restore() will
trample the prior contents. Future nested or concurrent snapshot usage
would need a different design (per-call allocation, or a small ring).

@ekryski ekryski left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's good. Noticed a few small nits, and adding some more tests. I'll update this PR directly and will merge when CI is green. Thanks @TheTom! 🔥

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@TheTom We may decide to drop these in future (likely). Fine for now as we're developing so let's leave it and the other file in this PR.

But really this is what ffai bench is for. I don't want to get into the habit of abusing integration tests and benching infrastructure. Leads to lost of complications and overhead that we had to deal with in my mlx-swift-lm fork.

If functionality is missing in the benchmark runner, I'd prefer to see us land updates there so it can be used for benching the way we need.

Comment thread Sources/FFAI/Generation/Drafter.swift Outdated
Comment thread Sources/FFAI/Generation/SpecDecode.swift
Comment thread Sources/FFAI/KVCache/CacheSnapshot.swift
Comment thread Sources/FFAI/Models/Text/LFM2Text.swift
Comment thread Sources/FFAI/Models/MoELayer.swift Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This file is humongous. We should see if we can shrink it to make it more efficient and/or split out into separate files grouped either by model family, architecture layer, or ops or something. Not a blocker here. Something noted for future.

ekryski added 3 commits May 26, 2026 23:44
Remove all "ITER N" / "(PR10)" / "PR10" markers from comments in
the model and op-wiring code. These were references to an authoring
agent's internal iteration log and don't help a future reader — they
made comments harder to skim and left a trail of stale cross-refs.

Where the surrounding comment depended on the marker for grammar or
context, tighten the prose so the remaining sentence still reads
cleanly. No behavioural changes; the comments still describe the
same fast-paths, gating env vars, and dispatch shapes.
For files Tom co-authored (modifications on top of an existing Eric-
copyrighted file), add "and Tom Turney (@TheTom)" to the existing
copyright line.

For files Tom wrote from scratch, mark Tom as the sole copyright
holder. SpecDecode.swift, CacheSnapshot.swift, and DrafterTests.swift
were missing the standard Apache 2.0 boilerplate — add it now.

No code changes.
PR #10's regression was the wrapper accepting blocks=2 (kernel
pass-2 hardcodes a 32-lane block-merge reduction, so anything below
32 silently produces zero output). PR #14 closed it with a
`blocks >= 32 && blocks % 32 == 0` precondition.

The other half of the contract — that at the minimum legal
blocks=32, two-pass output matches the canonical single-pass kernel
within tolerance — had no host-side test. Add one. metaltile's per-
kernel GPU correctness test would not have caught the dispatch-
routing bug; the wrapper is what needs covering.
@ekryski
ekryski merged commit 210efc8 into dev May 27, 2026
5 checks passed
@ekryski
ekryski deleted the tom/bagel-clean branch May 27, 2026 06:21
ekryski added a commit that referenced this pull request May 27, 2026
CI (Xcode 16.4 / Swift 5.10) was emitting ~70 warnings the local
toolchain (Xcode 26.5 / Swift 6.3) doesn't surface. Mechanical
cleanup pass — every change is a syntactic let-mutability fix, a
deletion of a dead binding, or a one-line typo correction. No
behavioural change.

Bulk fix — Accelerate ILP64 BLAS interface
- Package.swift: add ACCELERATE_NEW_LAPACK + ACCELERATE_LAPACK_ILP64
  swiftSettings on the FFAI target. cblas_sgemm / cblas_sgemv were
  deprecated in macOS 13.3 in favour of these headers; without the
  define every audio DSP file that wraps a BLAS call (DeepFilterNet,
  DeepFilterNetDSP, LFMAudio, GraniteSpeech, vision tower CPU
  attention, …) emitted a deprecation warning per build pass.

Per-file mechanical edits (~62 sites)
- var → let where the variable is never mutated (28 sites; LFMAudio
  tuple destructurings dominate, plus DACVAE, DeepFilterNet,
  FireRedVAD, Sortformer).
- Delete unused immutable let-bindings with side-effect-free RHS
  (22 sites; DeepFilterNet 13, FireRedVAD 3, Parakeet 2, FastVLMVision
  2, LFMAudio 1, Qwen3xText 1).
- DeepFilterNet linInW / linOutW: `if let X = weights["…"]` where
  X was never used — rewrite as `weights["…"] != nil` existence
  check; the body re-loads via groupedLinear with the same key.
- Parakeet padded: pure-RHS array build never read, deleted entirely.
- GraniteSpeech copyF32ToTensor: prefix `_ =` on three
  withUnsafeBytes calls whose results weren't consumed.
- FireRedVAD pickle decoder: drop duplicate `case 0x28:` and `case
  0x32:` lines that were already handled by previous patterns.
- FastVLMVision: `M_SQRT1_2` is deprecated; use `0.5.squareRoot()`.
- GPTOSSText: qualify `ReasoningLevel.none` so the compiler doesn't
  resolve to `Optional<…>.none` instead.
- CacheSnapshot: remove the dead `ConvStateCache` cast branches in
  both snapshotAll and restoreAll — `ConvStateCache` doesn't conform
  to `LayerCacheProtocol`; only `Qwen35GDNLayerCache.conv` and
  `Mamba2LayerCache.conv` wrap it, and they're already handled by
  separate branches. The `LayerCacheSnapshot.conv` enum case stays
  in place (it's public API).
- Loader/Model.swift: strip a stale rebase note from PR #14 that
  doesn't add information at the new HEAD.

Out of scope here
- ~119 Sendable / non-sendable closure capture warnings in CPU DSP +
  vision/audio CPU helpers. Need per-call-site review (the captures
  ARE thread-safe at runtime — DispatchQueue.concurrentPerform with
  disjoint indices — but Swift 6 strict concurrency flags them).
  Tracked as a follow-up commit on this branch.
- ~5151 MSL `unused variable` warnings in
  Sources/MetalTileSwift/Resources/kernels/*.metal — codegen output
  from metaltile, not FFAI source. Tracked separately in
  session-plan.md's queued items.

Verified
- make build green
- make format-check green
- No swift-format / lint regressions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants