spark_runtime/buffers/sizes.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Byte sizes for the per-pass GPU buffer arena.
4
5use atlas_core::config::ModelConfig;
6use atlas_kernels::attn_splitk;
7
8use super::sizes_q12::{Q12_SIZING_STREAMS, q12_batched_scratch_bytes};
9
10/// The widest `M` the FUSED dense-FFN gate+up decode GEMM serves (#927), and
11/// therefore the row extent `ffn_gate_up_fused` is sized for.
12///
13/// 16 — the top of the decode band. The fused arm is a per-LAUNCH saving, and
14/// the launch overhead it removes is only material while the GEMM is
15/// weight-bandwidth bound; at the prefill widths the same two projections
16/// already run at 68.6% of FP8 peak (nsys round 13, M=4576), where a launch
17/// costs nothing measurable. 16 is also the largest batch H100 round 13
18/// captured (`Captured CUDA graph for batch size 16`).
19///
20/// DECLARED HERE because the arena is sized in this crate and the dispatch
21/// rule lives above it; `spark_model::layers::dense_ffn_gateup_fused` reads
22/// THIS constant rather than restating it, so the band and the buffer cannot
23/// disagree.
24pub const GATEUP_FUSED_MAX_M: usize = 16;
25
26/// Byte sizes of each buffer, derived from ModelConfig.
27#[derive(Debug, Clone)]
28pub struct BufferSizes {
29 pub hidden_states: usize,
30 pub residual: usize,
31 pub norm_output: usize,
32 pub qkv_output: usize,
33 pub attn_output: usize,
34 pub gate_logits: usize,
35 /// FP32 gate logits [m, num_experts] for the ATLAS_FP32_GATE routing path.
36 /// Keeps the router GEMM accumulator unrounded into top-K so near-tied
37 /// experts don't flip on a BF16 store. Allocated whenever num_experts > 0.
38 pub gate_logits_f32: usize,
39 /// FP32 MoE-input norm output [m, hidden] for ATLAS_FP32_ROUTING — the
40 /// full-precision router_in the gate GEMM consumes. Allocated when experts > 0.
41 pub moe_router_in_f32: usize,
42 pub moe_output: usize,
43 pub logits: usize,
44 pub ssm_qkvz: usize,
45 pub ssm_ba: usize,
46 pub ssm_deinterleaved: usize,
47 pub ssm_gates: usize,
48 pub ssm_conv_out_f32: usize,
49 pub scratch: usize,
50 pub expert_gate_out: usize,
51 pub expert_up_out: usize,
52 pub expert_down_out: usize,
53 pub splitk_workspace: usize,
54 /// GDN FLA chunked-prefill scratch (single buffer, sub-divided W|U|S|uc).
55 /// 0 unless the model is a 128-dim-linear-head GDN model (ATLAS_GDN_FLA path).
56 pub gdn_fla_scratch: usize,
57 /// Mamba-2 SSD chunked-scan scratch (single buffer, sub-divided dt | dA_cumsum | CB).
58 /// 0 unless the model has Mamba-2 SSM layers. Shared across layers: they run
59 /// sequentially on one stream, so one allocation serves all 40.
60 pub ssd_scratch: usize,
61 /// Grouped O-projection latent: `[M, o_groups*o_lora_rank]` BF16 (V4-Flash).
62 /// 256 (placeholder) when `o_groups == 0`.
63 pub o_latent: usize,
64 /// Zero-filled BF16 weight (length max_dim) for unweighted RMSNorm under the
65 /// offset-from-1 kernel convention (scale = 1+weight → 1.0). DeepSeek-V4 q_b_norm.
66 pub norm_unit_w: usize,
67 /// HC residual streams: `[M, hc_mult, hidden]` BF16 (DeepSeek-V4 mHC).
68 /// 256 (placeholder) when `hc_mult == 0`.
69 pub hc_streams: usize,
70 /// HC `post` mixing weights: `[M, hc_mult]` F32.
71 pub hc_post: usize,
72 /// HC `comb` Sinkhorn matrix: `[M, hc_mult, hc_mult]` F32.
73 pub hc_comb: usize,
74 /// Low-rank mHC split-collapse scratch (Qwen3.8-Flash-Next): the staged
75 /// normed vector `[T, hc_mult*hidden]` F32 plus the rank vector
76 /// `[T, hc_lowrank]` F32, for SMALL T only — decode runs the collapse as
77 /// three multi-block launches because `grid=[1]` starves the fused kernel
78 /// (measured 2.0 ms/call, one SM's bandwidth). Sized for 64 tokens; the
79 /// dispatcher falls back to the fused kernel above that.
80 pub hc_lowrank_scratch: usize,
81 /// QSA stage-2 prefill-selection scratch (Qwen3.8-Flash-Next), SHARED
82 /// across the 12 indexer layers (they run serially). Layout, slabbed at
83 /// 2048 selective rows: qk [2048, (n_heads+1)*hd] BF16, q_post
84 /// [2048, n_heads, hd] F32, scores [2048, max_seq/ratio] F32, lists
85 /// [2048, topk] i32. 256 (placeholder) when no indexer.
86 pub qsa_select_scratch: usize,
87 /// Token IDs `[M]` u32 for the current pass — stable across the layer loop
88 /// so DeepSeek-V4 hash-MoE layers can read `tid2eid[token_id]`. Always
89 /// allocated (small); unused by models without hash routing.
90 pub token_ids: usize,
91 /// Dense-FFN activation-quant scratch, SHARED across all layers by the
92 /// MMQ (Q4_K), int8 (W4A8), and NVFP4 (W4A4) prefill paths. Was previously a
93 /// per-`DenseFfnLayer` field → 64× duplication (18 GB on Qwen3.6-27B) that
94 /// OOM'd chunked prefill layer-by-layer. Sized for the largest projection K.
95 /// `ffn_act_q8`: q8_1_mmq activations `m*kpad*4 + 1MB` (Q4_K path).
96 /// `ffn_act_a`: int8 `[m,K]` / NVFP4 packed `[m,K/2]` activations.
97 /// `ffn_act_scale`: int8 `[m,K/32]*4` / NVFP4 `[m,K/16]` group scales.
98 /// 0 for MoE models (dense FFN prefill path is Dense-only).
99 pub ffn_act_q8: usize,
100 pub ffn_act_a: usize,
101 pub ffn_act_scale: usize,
102 /// `[K/128, ceil16(M)]` FP32 copy of `ffn_act_scale` in the layout cuBLASLt
103 /// documents for a VEC128 B operand (token index contiguous). Written by
104 /// `fp8_act_scale_to_kmajor` on every cuBLASLt block-scaled FFN GEMM; the
105 /// quantizer's own `[M, K/128]` output stays in `ffn_act_scale` because the
106 /// in-tree kernel reads that order. 0 for MoE models, like its siblings.
107 pub ffn_act_scale_kmajor: usize,
108 /// `[GATEUP_FUSED_MAX_M, 2 * intermediate]` BF16 output of the FUSED
109 /// dense-FFN gate+up decode GEMM (#927) — the single cuBLASLt call at
110 /// `N = 2 * intermediate` whose row is `[gate | up]`. Its own buffer and
111 /// not a widened `expert_gate_out` because the fused arm serves the DECODE
112 /// band only (5..=16 rows, `layers/dense_ffn_gateup_fused.rs`): sizing it
113 /// for the band is ~2.2 MB at Qwen3.8-27B, sizing `expert_gate_out` for
114 /// `[max_batch_tokens, 2 * inter]` would be ~41 MB of prefill rows the arm
115 /// never writes.
116 ///
117 /// Allocated for every DENSE model rather than behind the lever: the arena
118 /// is built from `ModelConfig` and a target's serving levers are resolved
119 /// above this crate, and 2.2 MB is not worth a second resolution that
120 /// could disagree with the dispatch site's.
121 pub ffn_gate_up_fused: usize,
122 /// FP8 block-scaled activation scratch for prefill projections (qkv / o /
123 /// ssm-qkvz). Persistent so the W8A8+FP32-epilogue path stops doing a
124 /// per-projection cuMemAlloc + cuStreamSynchronize + cuMemFree. 1 byte/elem.
125 pub fp8_act: usize,
126 /// Per-128-block FP32 scales paired with `fp8_act` (one f32 per 128 elems).
127 pub fp8_act_scale: usize,
128 /// `[K/128, ceil16(M)]` FP32 transpose of `fp8_act_scale` — the VEC128
129 /// B-scale layout cuBLASLt documents (token index contiguous). The
130 /// prefill-projection sibling of `ffn_act_scale_kmajor`, allocated for
131 /// every model rather than dense-only because the SSM `in_proj_qkvz`
132 /// cuBLASLt arm consumes it. Same element count as `fp8_act_scale`.
133 pub fp8_act_scale_kmajor: usize,
134 /// LoRA shrink output `xa = x@Aᵀ`: [m, adapter_max_rank] BF16.
135 /// 0 (→ NULL alloc) when no adapter is configured (adapter_max_rank == 0).
136 pub lora_xa: usize,
137 /// LoRA expand output `delta = xa@Bᵀ`: [m, max target n_out] BF16, where
138 /// max n_out = max(hidden, intermediate) — covers k/v/o/gate/up/down in
139 /// v0 (q_proj is excluded). 0 (→ NULL) when no adapter.
140 pub lora_delta: usize,
141 /// LoRA hidden-activation scratch [m, intermediate_size] BF16 for the
142 /// runtime delta path on FFN projections. 0 (→ NULL) when no adapter.
143 pub lora_hact: usize,
144 /// LoRA per-request routing slots `[m]` i32 — one adapter SLOT index per
145 /// prefilling token (all equal for a single-request prefill; resolves
146 /// `-1`→active before upload). Dedicated buffer (not a packed meta offset)
147 /// so the m-element prefill slot array never collides with the per-path
148 /// positions/slots/block_table region. 0 (→ NULL) when no adapter
149 /// (adapter_max_rank == 0).
150 pub lora_seq_slot: usize,
151 /// Native keep-packed Q2_0 prefill transient-dequant scratch
152 /// (`ATLAS_GGUF_NATIVE_Q2=1`). ONE persistent BF16 `[N,K]` buffer sized to
153 /// the LARGEST keep-packed projection, REUSED for every per-projection
154 /// dequant so prefill stops doing a per-matmul cuMemAlloc +
155 /// cuStreamSynchronize + cuMemFree (the multi-second fixed cost behind the
156 /// 3.7 s / 28-token TTFT regression). 0 (→ NULL) unless the flag is set.
157 pub q2_dequant_scratch: usize,
158 /// Native Q2_0 MMQ prefill q8_1 activation scratch (`ATLAS_GGUF_NATIVE_Q2_MMQ=1`).
159 /// ONE persistent q8_1_mmq buffer (`m*kpad*4 + 1MB`) shared by every kept-packed
160 /// projection (FFN gate/up/down, attn q/k/v/o, GDN qkvz): each seam quantizes
161 /// its BF16 activation into this buffer then runs the packed MMQ GEMM — so the
162 /// 2-bit weight is never dequantized to a BF16 scratch (kills the ~2s dequant
163 /// tax AND the shared-`q2_dequant_scratch` co-dispatch race). Sized to the
164 /// widest projection K = max(hidden, intermediate, q_heads*head_dim).
165 /// 0 (→ NULL) unless the MMQ sub-flag is set.
166 pub q2_act_q8: usize,
167 /// Row-wise FP8 GDN prefill BF16-weight slab (`ATLAS_FP8_ROWWISE=1`).
168 /// ONE arena allocation holding the BF16 dequant of EVERY GDN layer's
169 /// per-row `in_proj_qkvz` + `out_proj`, bump-carved one slice per layer
170 /// on that layer's first prefill and never freed. Replaces the lazy
171 /// `gpu.alloc` memoised by weight pointer that the #917 H100 receipt
172 /// caught at `167772160` B per layer outside this ledger. 0 (→ NULL)
173 /// unless the lever is armed; sizing lives in `sizes_rowwise.rs`.
174 pub ssm_rowwise_w_bf16: usize,
175}
176
177impl BufferSizes {
178 /// Compute all buffer sizes from model config and max batch tokens.
179 ///
180 /// All sizes in bytes. BF16 = 2 bytes per element.
181 /// Logits buffer is capped: only needed for decode (1 token) or
182 /// speculative verification (K tokens), never for full prefill.
183 ///
184 /// `max_seq_len` and `kv_block_size` are needed to size the scratch
185 /// buffer for block table metadata during batched decode / verify.
186 pub fn from_config(
187 config: &ModelConfig,
188 max_batch_tokens: usize,
189 max_seq_len: usize,
190 kv_block_size: usize,
191 max_batch_size: usize,
192 ) -> Self {
193 // Derived batched-decode metadata layout (rows = max(32, bs)).
194 // Byte-identical sizing for every bs <= 32; see `decode_meta.rs`.
195 let decode_meta = super::DecodeMetaLayout::for_max_batch_size(max_batch_size);
196 let bf16 = 2;
197 let m = max_batch_tokens;
198 let h = config.hidden_size;
199
200 // Q projection output: gated models produce [Q, gate] (2× nq*hd),
201 // ungated models (VL) produce only [Q] (nq*hd).
202 let q_heads = config.num_attention_heads;
203 let kv_heads = config.num_key_value_heads;
204 let hd = config.head_dim;
205 let q_proj_mul = if config.attn_gated { 2 } else { 1 };
206 let qkv_dim = (q_heads * q_proj_mul + 2 * kv_heads) * hd;
207
208 let top_k = config.num_experts_per_tok;
209
210 // Scratch layout (two users, take max):
211 //
212 // A) Prefill chunk metadata (after MoE routing data):
213 // [0 .. moe_scratch): MoE topK routing indices+weights
214 // [moe_scratch .. ): positions(m*4) + slots(m*8) + block_table(max_blocks*4) + seq_len(4)
215 //
216 // B) Batched decode/verify metadata:
217 // [0 .. 32768): fixed metadata region
218 // [32768 .. 32768+24R): decode metadata (positions, seq_slot,
219 // slots, seq_lens; R = decode-meta rows, `decode_meta.rs` —
220 // 24R = 768 at the 32-row floor)
221 // [32768+24R .. ): decode block table (padded_n × max_blocks × 4 B)
222 // Batched MTP verify (verify_e.rs) overlays the SAME base with
223 // VERIFY_ROW_CAP-row gaps at derived offsets (verify_e.rs VMETA_*),
224 // bt at +24R (bt_rows mirrors the cap).
225 // Each path re-uploads its own layout pre-dispatch; sizing takes
226 // the wider (verify) envelope.
227 //
228 // MoE scratch: 2 * M * top_k * 4 (indices [M*top_k] u32 + weights [M*top_k] f32)
229 let moe_scratch = 2 * m * top_k * 4;
230 let max_blocks = max_seq_len
231 .checked_div(kv_block_size)
232 .map(|q| q + 1)
233 .unwrap_or(256);
234 // Prefill metadata: mirrors exact layout in prefill_chunk(). MRoPE
235 // (Qwen3-VL / Qwen3.6) uploads THREE u32 position streams packed
236 // back-to-back (T, H, W); every other model uploads ONE. Sizing the
237 // scratch region for 1× with MRoPE active caused `cuMemcpyHtoDAsync_v2
238 // status 1` failures on long-context prefills (observed: 16k Qwen3.6
239 // failed, 8k passed because the extra 64 KB of write overflow happened
240 // to still land inside the over-provisioned `moe_scratch + meta`
241 // aggregate).
242 let pos_streams = if config.mrope_interleaved { 3 } else { 1 };
243 let pos_bytes = m * 4 * pos_streams;
244 let slot_offset = (pos_bytes + 7) & !7;
245 let slot_end = slot_offset + m * 8;
246 let bt_offset = (slot_end + 3) & !3;
247 let bt_end = bt_offset + max_blocks * 4;
248 let sl_offset = (bt_end + 3) & !3;
249 let prefill_meta = sl_offset + 4;
250 // Block table metadata: the widest user is the batched MTP verify
251 // (verify_e.rs) at R = bt_rows (mirrors VERIFY_ROW_CAP; was 96, the wave-11
252 // depth-at-width envelope — 32:2), whose bt staging sits at
253 // meta_base+2048 (wider 96-row gaps: positions 384 | seq_slot 384 |
254 // slots 768 | seq_lens 384). Batched decode (padded_n ≤ 32) and
255 // DFlash K=γ+1=17 verify keep the narrow +768 layout — strictly
256 // inside this envelope.
257 let bt_rows = 160usize; // batched verify R cap (VERIFY_ROW_CAP, verify_e2.rs)
258 // Envelope = max(verify 96-row overlay, DERIVED decode layout).
259 // The decode layout (`decode_meta.rs`, rows = max(32, bs)) sits
260 // strictly inside the verify overlay for every rows <= 64 (bt at
261 // 24R <= 1536 < 2048, rows <= 96), so this max() changes NOTHING
262 // for bs <= 64; it only grows the scratch once rows > ~85.
263 let bt_meta = 32768
264 + (bt_rows * 24 + bt_rows * max_blocks * 4).max(decode_meta.meta_bytes(max_blocks));
265 let scratch_min = 64 * 1024;
266 // Q12 kernel-batched prefill stages N per-stream meta blocks plus a
267 // stacked BatchedAttnMetadata block — a strictly larger footprint than
268 // the single-stream `prefill_meta`. Provision for `Q12_SIZING_STREAMS`
269 // streams splitting the full token arena so the fast path stays
270 // available for deep-context concurrent prefills without overrunning
271 // scratch (#110: the unprovisioned N-stream multiplication overran the
272 // buffer, producing an out-of-range HtoD → sticky CUDA-700).
273 let q12_chunk = m.div_ceil(Q12_SIZING_STREAMS).max(1);
274 let q12_batched = q12_batched_scratch_bytes(
275 Q12_SIZING_STREAMS,
276 q12_chunk,
277 top_k,
278 config.mrope_interleaved,
279 );
280 let scratch = scratch_min
281 .max(moe_scratch + prefill_meta)
282 .max(bt_meta)
283 .max(q12_batched);
284
285 // Batched expert output buffers for MoE (or dense FFN).
286 // Sized for max(K=3 verify, prefill chunk) × top_k experts.
287 //
288 // The row extent is rounded UP to a multiple of 16 because the FP8
289 // block-scaled cuBLASLt GEMM (`ops::cublas_fp8_proj*`, used by the
290 // W8A8 dense-FFN prefill added for #917/#928) cannot be handed a raw
291 // M: cuBLASLt rejects a scale-tensor M extent that is not a multiple
292 // of 4, so the helper pads M to 16 and the matmul writes those phantom
293 // rows into the output. Without the pad here, a prefill chunk that is
294 // exactly `max_batch_tokens` would write up to 15 rows PAST
295 // `expert_gate_out` — straight into the neighbouring arena buffer.
296 // Costs <= 15 * intermediate * 2 B per buffer (~0.5 MB on a 27B), which
297 // is cheaper than a second output allocation or a per-call bounce.
298 // The row extent every buffer a cuBLASLt block-scaled FP8 GEMM touches
299 // must be sized for. `ops::cublas_fp8_proj_prequant` hands the library
300 // `ceil16(M)`: the phantom activation/scale rows are READ and the
301 // phantom output rows are WRITTEN. SSOT for the pads below.
302 let m_pad = m.div_ceil(16) * 16;
303 let k_max = m.max(3).div_ceil(16) * 16; // prefill chunk or K=3 verify, +cuBLASLt M-pad
304 let expert_inter = if config.num_experts > 0 {
305 let routed = config.num_experts_per_tok * config.moe_intermediate_size;
306 k_max * routed.max(config.intermediate_size)
307 } else {
308 k_max * config.intermediate_size
309 };
310 let expert_gate_out = expert_inter * bf16;
311 let expert_up_out = expert_inter * bf16;
312 // Routed expert down output: [k_max * top_k, moe_input_size].
313 // For LatentMoE (Super 120B), routed experts output in latent space.
314 let moe_out_dim = config.moe_input_size();
315 let expert_down_out = if config.num_experts > 0 {
316 k_max * config.num_experts_per_tok * moe_out_dim * bf16
317 } else {
318 k_max * h * bf16
319 };
320
321 // Logits: only last token used during prefill. Cap at 160 tokens —
322 // the batched MTP verify's R = Σ ks row cap (n=32 × k=3 rows, the
323 // wave-11 depth-at-width envelope; VERIFY_ROW_CAP in verify_e2.rs).
324 // This also covers decode=1, batched decode padded_n<=32 PLUS the
325 // run_standard mixed path (`decode_b2`) parking prefill logits at
326 // row `padded_n` = 32 (the old 33-row bound), spec_verify≤5, and
327 // DFlash K=γ+1=17. ~45 MB at vocab 248320 (was ~30 MB at 64 rows,
328 // ~16 MB at 33).
329 // Derived floor for wide native batches: the run_standard mixed path
330 // (`decode_b2`) parks prefill logits at row `padded_n`, which can be
331 // as high as `decode_meta.rows()` — so the arena must hold rows+1.
332 // Inert (160) for every rows <= 159, i.e. all bs <= 159.
333 let logits_tokens = m.min(160.max(decode_meta.rows() + 1));
334
335 // Mamba-2 d_inner may exceed hidden_size; norm_output and attn_output must fit.
336 let mamba2_d_inner = config.mamba2_d_inner();
337 let max_dim = h.max(mamba2_d_inner);
338
339 // Split-K decode workspace: one `[o[head_dim], m, l]` F32 slot per
340 // (sequence, q head, split). The split-K kernel addresses
341 // `((seq * q_heads) + head) * num_splits + split`, so a short
342 // allocation here is an out-of-bounds DEVICE WRITE with no error —
343 // which is why the slot count comes from the same pure function the
344 // dispatch picks `num_splits` with (`atlas_kernels::attn_splitk`,
345 // #928) rather than from a literal restated here.
346 //
347 // The bound is `DecodeMetaLayout::rows()`, not the pinned max batch:
348 // rows is the widest batch the metadata upload accepts and therefore
349 // the real ceiling on `num_seqs`.
350 //
351 // Under the `legacy` policy — every target but Hopper — this is
352 // `sm_count` slots, i.e. the ~48 KB it has always been: that rule
353 // divides the SM count by `q_heads * reference batch`, so the product
354 // can never exceed it. Under `auto` it is `rows * q_heads * splits`
355 // (3.2 MB at the H100 27B shape), which buys the C=1 occupancy the
356 // whole lever is for.
357 let splitk_slots = attn_splitk::workspace_slots(
358 attn_splitk::policy_from_env(),
359 atlas_kernels::TARGET_SM_COUNT,
360 q_heads as u32,
361 decode_meta.rows() as u32,
362 (max_batch_size as u32).max(1),
363 ) as usize;
364 let splitk_workspace = splitk_slots * (hd + 2) * 4;
365
366 // The residual stream is always BF16.
367 let residual_elem = bf16;
368
369 // FP8 block-scaled activation scratch for prefill projections. The
370 // widest contract dim across call sites is hidden (qkv / ssm-qkvz) or
371 // q_heads*head_dim (o_proj). 1 byte/elem fp8 + one f32 per 128-block.
372 // Mamba-2 out_proj contracts over d_inner (may exceed hidden), and its
373 // prefill input is FP8-precast into this buffer.
374 // ...and the GDN `out_proj`, which contracts over `value_dim`. It
375 // happens to equal `q_heads * hd` on Qwen3.8-27B (6144), so naming it
376 // changes no allocation there — but the W8A8 cuBLASLt arm added in
377 // #928 quantizes into this buffer, and a model whose value_dim is the
378 // widest contract would otherwise size it short and silently fall back.
379 let max_proj_k = h
380 .max(q_heads * hd)
381 .max(mamba2_d_inner)
382 .max(config.linear_num_value_heads * config.linear_value_head_dim);
383 // Padded to 16 rows: `ops::cublas_fp8_proj` hands cuBLASLt `ceil16(M)`
384 // and the matmul reads those phantom activation/scale rows.
385 let fp8_act = m_pad * max_proj_k;
386 let fp8_act_scale = m_pad * max_proj_k.div_ceil(128) * 4;
387 // The cuBLASLt arm reads the SAME scales transposed, so both layouts are
388 // live at once and cannot share a buffer. ~0.2 MB at m=2048, K=5120 —
389 // against the 167772160 B/layer off-ledger BF16 weight dequant it
390 // replaces (#917 H100 receipt, 2026-09-11).
391 let fp8_act_scale_kmajor = fp8_act_scale;
392 // LoRA scratch — only when an adapter is configured (adapter_max_rank
393 // set programmatically pre-build). Widest target n_out =
394 // max(hidden, intermediate, q_proj): covers k/v, o/down (hidden),
395 // gate/up (intermediate), and gated q_proj (2*q_heads*head_dim, which
396 // can exceed both — e.g. 35B 2*16*256=8192 > hidden 4096).
397 let (lora_xa, lora_delta, lora_hact, lora_seq_slot) = if config.adapter_max_rank > 0 {
398 let max_n = h
399 .max(config.intermediate_size)
400 .max(q_proj_mul * q_heads * hd);
401 (
402 m * config.adapter_max_rank * bf16,
403 m * max_n * bf16,
404 m * config.intermediate_size * bf16,
405 m * 4, // [m] i32 per-request routing slots (prefill path)
406 )
407 } else {
408 (0, 0, 0, 0)
409 };
410
411 // GDN FLA chunked-prefill scratch — ONE buffer holding W|U|S|uc back-to-back,
412 // sized for the chunked-prefill arena (nt = ceil(max_batch_tokens / CHUNK)).
413 // Only the 128-dim-linear-head GDN path uses it (the FLA kernels are compiled
414 // for K_DIM=V_DIM=128); 0 otherwise so BufferArena allocs NULL and the
415 // ATLAS_GDN_FLA dispatch stays disabled. Layout per region:
416 // W [nt*nv][CHUNK][kd] bf16 ; U,uc [nt*nv][CHUNK][vd] bf16 ;
417 // S [nt*nv][kd][vd] bf16 ; gc [nt*nv][CHUNK] f32.
418 const FLA_CHUNK: usize = 64;
419 // SSD chunked scan (mamba2_ssd_*): dt[H][nc][L] f32 + dA_cs[H][nc][L] f32
420 // + CB[nc][G][L][L] f32, L = 64.
421 const SSD_L: usize = 64;
422 let ssd_scratch = if config.mamba_num_heads > 0 && config.ssm_state_size > 0 {
423 let nc = m.div_ceil(SSD_L) + 1;
424 let hh = config.mamba_num_heads;
425 let gg = config.n_groups.max(1);
426 (hh * nc * SSD_L * 4) * 2 + nc * gg * SSD_L * SSD_L * 4
427 } else {
428 0
429 };
430
431 let gdn_fla_scratch = if config.linear_num_value_heads > 0
432 && config.linear_key_head_dim == 128
433 && config.linear_value_head_dim == 128
434 {
435 // +margin: the batched FLA path (ATLAS_GDN_BATCHED_FLA) sizes its
436 // regions by total_nt = batch*ceil(chunk_len/64), which can exceed
437 // ceil(m/64) by up to `batch` chunks due to per-stream last-chunk
438 // rounding. 16 covers the co-dispatch max-seqs.
439 let nt = m.div_ceil(FLA_CHUNK) + 16;
440 let nv = config.linear_num_value_heads;
441 let kd = config.linear_key_head_dim;
442 let vd = config.linear_value_head_dim;
443 let w = nt * nv * FLA_CHUNK * kd * bf16;
444 let u = nt * nv * FLA_CHUNK * vd * bf16;
445 let s = nt * nv * kd * vd * bf16;
446 let uc = nt * nv * FLA_CHUNK * vd * bf16;
447 let gc = nt * nv * FLA_CHUNK * 4;
448 w + u + s + uc + gc
449 } else {
450 0
451 };
452
453 // Native keep-packed Q2_0 prefill scratch (Tier-1 transient-dequant +
454 // Tier-2 MMQ q8_1 activation); env-gated, 0 unless the flags are set.
455 // Sizing rationale + bounds live on `sizes_q2::q2_scratch_sizes`.
456 let (q2_dequant_scratch, q2_act_q8) = super::sizes_q2::q2_scratch_sizes(config, m, h, hd);
457
458 // Row-wise FP8 GDN prefill BF16-weight slab; env-gated, 0 unless
459 // `ATLAS_FP8_ROWWISE=1`. Sizing + the #917 receipt live on
460 // `sizes_rowwise::ssm_rowwise_w_bf16_bytes`.
461 let ssm_rowwise_w_bf16 = super::sizes_rowwise::ssm_rowwise_w_bf16_bytes(config);
462
463 // Dense-FFN activation-quant scratch, shared across all layers (SSOT).
464 // Sized for the largest projection K = max(hidden, intermediate); the
465 // dense_ffn prefill paths pass `h.max(inter)` to the requant kernels.
466 // 0 for MoE (num_experts>0) — those never take the dense_ffn MMQ path.
467 // Fused gate+up decode GEMM output (#927): `[ceil16(MAX_M), 2*inter]`
468 // BF16. `ceil16` because `cublas_fp8_proj_prequant` hands cuBLASLt
469 // `ceil16(M)` and the phantom rows are WRITTEN — the same headroom
470 // `expert_gate_out` carries, for the same reason. Dense models only;
471 // MoE never reaches the dense-FFN arm.
472 let ffn_gate_up_fused = if config.num_experts == 0 {
473 let rows = GATEUP_FUSED_MAX_M.div_ceil(16) * 16;
474 rows * 2 * config.intermediate_size * bf16
475 } else {
476 0
477 };
478
479 let (ffn_act_q8, ffn_act_a, ffn_act_scale, ffn_act_scale_kmajor) =
480 if config.num_experts == 0 {
481 let kmax = h.max(config.intermediate_size);
482 let kpad = kmax.div_ceil(256) * 256;
483 // `m_pad` (above) is the cuBLASLt row extent: the W8A8
484 // dense-FFN prefill (#917/#928) hands cuBLASLt `ceil16(M)` and
485 // the matmul READS the phantom activation rows (they are
486 // zeroed, but they are read). It also covers every unpadded
487 // consumer of this scratch.
488 (
489 m * kpad * 4 + (1 << 20), // q8_1_mmq: m*kpad*4 + 1MB (matches q8_1_scratch_bytes)
490 m_pad * kmax, // int8 a_i8 [m,K] ≥ NVFP4 packed [m,K/2] ≥ fp8 [m,K]
491 m_pad * (kmax / 32) * 4, // int8 a_scale [m,K/32]*4 ≥ fp8 [m,K/128]*4
492 // Transposed VEC128 activation scales for the cuBLASLt arm:
493 // one f32 per (128-of-K group, padded token). Same element
494 // count as the fp8 use of `ffn_act_scale`, a quarter of the
495 // int8 one — ~0.65 MB at max_batch_tokens=1193, K=17408.
496 m_pad * (kmax / 128) * 4,
497 )
498 } else {
499 (0, 0, 0, 0)
500 };
501
502 Self {
503 hidden_states: m * h * residual_elem,
504 residual: m * h * residual_elem,
505 norm_output: m * max_dim * bf16,
506 // `m_pad`, not `m`: the cache-skip Q/K/V prefill's cuBLASLt arm
507 // WRITES `ceil16(M)` rows of `q_proj` here (readers still touch
508 // only the real M). Same headroom `ssm_qkvz` and `moe_output`
509 // already carry, and the reason is the same one (#928). ~0.4 MB on
510 // a 27B.
511 qkv_output: m_pad * qkv_dim * bf16,
512 attn_output: (m * config.num_attention_heads * config.head_dim * bf16)
513 .max(m * mamba2_d_inner * bf16)
514 // MLA absorbed: attention output is [M, nq, mla_cache_dim=kv_lora+rope]
515 .max(if config.kv_lora_rank > 0 {
516 m * config.num_attention_heads
517 * (config.kv_lora_rank + config.qk_rope_head_dim)
518 * bf16
519 } else {
520 0
521 }),
522 gate_logits: if config.num_experts > 0 {
523 // LongCat zero-experts: the router scores (routed + zero)
524 // logits even though only `num_experts` expert FFNs exist.
525 m * (config.num_experts + config.zero_expert_num) * bf16
526 } else {
527 256
528 },
529 gate_logits_f32: if config.num_experts > 0 {
530 m * (config.num_experts + config.zero_expert_num) * 4
531 } else {
532 256
533 },
534 moe_router_in_f32: if config.num_experts > 0 {
535 m * h * 4
536 } else {
537 256
538 },
539 // Same cuBLASLt FP8 M-pad headroom as `k_max` above: the dense-FFN
540 // down projection writes its [M, hidden] result here.
541 moe_output: m.div_ceil(16) * 16 * h * bf16,
542 logits: logits_tokens * config.vocab_size * bf16, // BF16 from LM head kernel
543 // SSM buffers are also reused by attention prefill/multi-seq as scratch:
544 // ssm_qkvz: K+V contiguous storage in prefill [M, 2*kv_dim]
545 // Mamba-2 in_proj output [M, in_proj_size]
546 // ssm_deinterleaved: Q contiguous copy [M, nq*hd]
547 // Mamba-2 conv1d output [M, d_xBC]
548 // Use max across all uses with minimum 256 to avoid 0-byte alloc.
549 // `m_pad`, not `m`: the SSM `in_proj_qkvz` cuBLASLt arm WRITES
550 // `ceil16(M)` output rows here (readers still touch only the real
551 // M). Without it a chunk exactly `max_batch_tokens` wide spills up
552 // to 15 rows into the NEXT arena buffer. ~0.4 MB on a 27B.
553 ssm_qkvz: (m_pad * config.ssm_qkvz_size() * bf16)
554 .max(m * config.mamba2_in_proj_size() * bf16)
555 // `k` at row 0 and `v` at row `m`, each `ceil16(M)` rows
556 // tall on the cuBLASLt arm: the furthest byte is
557 // `(m + m_pad) * kv_dim` (#928, `prefill_qkv_w8a8.rs`). Was
558 // `m * 2 * kv_dim`, which this is never smaller than.
559 .max((m + m_pad) * kv_heads * hd * bf16)
560 .max(m * config.shared_expert_intermediate_size * bf16) // MoE shared up scratch
561 .max(256),
562 ssm_ba: (m * config.ssm_ba_size() * bf16)
563 .max(m * config.moe_latent_size * bf16) // LatentMoE latent buffer
564 // MLA reuses ssm_ba for two separate buffers:
565 // - q_latent [M, q_lora_rank] BF16 — output of wq_a GEMM
566 // - k_rope_buf [M, qk_rope_head_dim] BF16 — output of wkv_a_rope GEMM
567 // Both are written sequentially (q_latent is consumed before
568 // k_rope_buf is allocated). Size for the larger of the two.
569 .max(if config.kv_lora_rank > 0 {
570 (m * config.qk_rope_head_dim * bf16).max(m * config.q_lora_rank * bf16)
571 } else {
572 0
573 })
574 .max(256),
575 // Same cuBLASLt M-pad as `ssm_qkvz`: on a `sequential_qkvz` model
576 // THIS is the projection's destination buffer.
577 ssm_deinterleaved: (m_pad * config.ssm_qkvz_size() * bf16)
578 .max(m * config.mamba2_d_xbc() * bf16)
579 .max(m * q_heads * hd * bf16)
580 // MLA absorbed: Q_absorbed buffer is [M, nq, mla_cache_dim=kv_lora+rope]
581 .max(if config.kv_lora_rank > 0 {
582 m * q_heads * (config.kv_lora_rank + config.qk_rope_head_dim) * bf16
583 } else {
584 0
585 })
586 .max(256),
587 ssm_gates: (m * config.linear_num_value_heads * 2 * 4).max(256),
588 // FP32 conv output for SSM recurrent path precision (4 bytes/element).
589 // Uses ssm_qkvz_size as upper bound (includes Q+K+V+Z).
590 // Also reused by MLA as q_rope contiguous buffer: [M, nq * qk_rope_head_dim] BF16.
591 ssm_conv_out_f32: (m * config.ssm_qkvz_size() * 4)
592 .max(if config.kv_lora_rank > 0 {
593 m * q_heads * config.qk_rope_head_dim * bf16
594 } else {
595 0
596 })
597 .max(256),
598 scratch,
599 expert_gate_out,
600 expert_up_out,
601 expert_down_out,
602 splitk_workspace,
603 gdn_fla_scratch,
604 ssd_scratch,
605 // Grouped O-projection latent (V4-Flash): [M, o_groups*o_lora_rank].
606 o_latent: (m * config.o_groups * config.o_lora_rank * bf16).max(256),
607 // Zero-filled weight for unweighted RMSNorm (q_b_norm).
608 norm_unit_w: max_dim * bf16,
609 // HC buffers: only allocated for DeepSeek-V4 (hc_mult > 0).
610 hc_streams: if config.hc_mult > 0 {
611 // FP32 mHC highway: the residual streams grow large across the
612 // blocks (the manifold-mixing is norm-preserving, eigenvalue 1),
613 // so BF16 storage swamps the small per-layer signal at scale and
614 // collapses generation. Store the streams in FP32 (4 bytes).
615 m * config.hc_mult * h * 4
616 } else {
617 256
618 },
619 hc_post: if config.hc_mult > 0 {
620 (m * config.hc_mult * 4).max(256)
621 } else {
622 256
623 },
624 hc_comb: if config.hc_mult > 0 {
625 (m * config.hc_mult * config.hc_mult * 4).max(256)
626 } else {
627 256
628 },
629 hc_lowrank_scratch: if config.hc_mult > 0 && config.hc_lowrank > 0 {
630 // Two exclusive layouts share this region:
631 // - decode split path (T <= 64): normed FP32 [64, hc*H] then
632 // low FP32 [64, rank];
633 // - prefill GEMM path (T > 64, slabbed at <= 2048 tokens):
634 // normed BF16 [Ts, hc*H], up_pre BF16 [Ts, hc*H],
635 // low BF16 [Ts, rank], inj_pre BF16 [Ts, hc].
636 let t = m.min(64);
637 let split = t * (config.hc_mult * h + config.hc_lowrank) * 4;
638 let ts = m.min(2048);
639 let gemm = ts * (2 * config.hc_mult * h + config.hc_lowrank + config.hc_mult) * 2;
640 split.max(gemm)
641 } else {
642 256
643 },
644 qsa_select_scratch: if config.index_topk > 0 && config.index_compress_ratio > 0 {
645 const ROWS: usize = 2048;
646 let qkw = (config.index_n_heads + 1) * config.index_head_dim;
647 let n_blocks = max_seq_len.div_ceil(config.index_compress_ratio);
648 let topk = config.index_topk / config.index_compress_ratio;
649 ROWS * qkw * 2
650 + ROWS * config.index_n_heads * config.index_head_dim * 4
651 + ROWS * n_blocks * 4
652 + ROWS * topk * 4
653 } else {
654 256
655 },
656 // Token IDs [M] u32 (stable across the layer loop for hash-MoE).
657 token_ids: (m * 4).max(256),
658 ffn_act_q8,
659 ffn_act_a,
660 ffn_act_scale,
661 ffn_act_scale_kmajor,
662 ffn_gate_up_fused,
663 fp8_act,
664 fp8_act_scale,
665 fp8_act_scale_kmajor,
666 lora_xa,
667 lora_delta,
668 lora_hact,
669 lora_seq_slot,
670 q2_dequant_scratch,
671 q2_act_q8,
672 ssm_rowwise_w_bf16,
673 }
674 }
675
676 /// Total bytes across all buffers.
677 pub fn total_bytes(&self) -> usize {
678 self.hidden_states
679 + self.residual
680 + self.norm_output
681 + self.qkv_output
682 + self.attn_output
683 + self.gate_logits
684 + self.gate_logits_f32
685 + self.moe_router_in_f32
686 + self.moe_output
687 + self.logits
688 + self.ssm_qkvz
689 + self.ssm_ba
690 + self.ssm_deinterleaved
691 + self.ssm_gates
692 + self.ssm_conv_out_f32
693 + self.scratch
694 + self.expert_gate_out
695 + self.expert_up_out
696 + self.hc_lowrank_scratch
697 + self.qsa_select_scratch
698 + self.expert_down_out
699 + self.splitk_workspace
700 + self.gdn_fla_scratch
701 + self.ssd_scratch
702 + self.hc_streams
703 + self.hc_post
704 + self.hc_comb
705 + self.token_ids
706 + self.ffn_act_q8
707 + self.ffn_act_a
708 + self.ffn_gate_up_fused
709 + self.ffn_act_scale
710 + self.ffn_act_scale_kmajor
711 + self.fp8_act
712 + self.fp8_act_scale
713 + self.fp8_act_scale_kmajor
714 + self.lora_xa
715 + self.lora_delta
716 + self.lora_hact
717 + self.lora_seq_slot
718 + self.q2_dequant_scratch
719 + self.q2_act_q8
720 + self.ssm_rowwise_w_bf16
721 }
722}