Skip to main content

ferrum_kernels/backend/
traits.rs

1//! Core Backend trait — the single abstraction over CUDA / Metal / CPU.
2
3use ferrum_types::{FerrumError, Result};
4
5pub use super::capabilities::{
6    BackendCollective, BackendGraph, BackendMoeFused, BackendQuantGguf, BackendQuantMarlin,
7};
8use super::dtype::Dtype;
9pub use super::types::MoeRouting;
10use super::types::{AttnConfig, KvCacheQuant, SrcDtype};
11
12/// Maximum decode-graph layer count. Per-layer call sites that share
13/// graph-captured host staging arrays use this as the stride between
14/// distinct slots. CUDA-only invariant (other backends ignore the
15/// `slot` argument); 64 covers all current LLM families up to and
16/// including Llama-3-70B (80 layers — but 70B doesn't run on a single
17/// 4090 anyway, so 64 is safe in practice for v0.2).
18pub const MAX_LAYERS_FOR_GRAPH: usize = 64;
19
20// Note: `TransformerConfig` / `AttnType` / `MlpType` / `RopeConfig` used to
21// live here when `ModelRunner` needed a generic model config. They're now
22// per-model (e.g. `Qwen3Config` in `ferrum-models::models::qwen3`) so each
23// model can carry exactly the architecture parameters it cares about.
24// Backend trait stays model-agnostic.
25
26/// The core abstraction over CUDA / Metal / CPU.
27///
28/// Key design: operations take a `&mut Self::Context` which accumulates work.
29///   - **CPU**: Context is `()` — ops execute immediately.
30///   - **Metal**: Context is a `CommandBuffer` — ops encode into it, flushed on `sync()`.
31///   - **CUDA**: Context is a `CudaStream` — ops launch on the stream, synced on `sync()`.
32///
33/// `layer_forward` passes the context through all ops in a layer.
34/// `ModelRunner` calls `sync()` only when it needs results (e.g., reading logits).
35pub trait Backend: Send + Sync + Sized + 'static {
36    type Buffer: Send + Sync;
37
38    /// Execution context that accumulates GPU work.
39    ///   - CPU: `()` (no-op, ops execute inline)
40    ///   - Metal: wraps a CommandBuffer
41    ///   - CUDA: wraps a CudaStream
42    type Context;
43
44    /// GPU-side timer scoped to this backend. See `super::timer` —
45    /// CPU: `Instant`; Metal: sync-wrap; CUDA: `cuEvent`.
46    /// PLAYBOOK § 1.1.
47    type Timer: super::timer::BackendTimer<Self>;
48
49    /// Factory for `Self::Timer` — exists so call sites that have a
50    /// `<B: Backend>` parameter can spawn a timer without importing the
51    /// concrete impl. PLAYBOOK § 1.2.
52    fn make_timer() -> Self::Timer;
53
54    /// Opaque per-backend GPTQ weight representation.
55    ///   - CPU: dequantized f32 weights (run as regular GEMM)
56    ///   - Metal: `()` — unsupported; `gemm_gptq` errors
57    // Note (Phase 3e/4 + Phase C):
58    // - `type QuantStore` (GGUF k-quant storage) was removed in Phase 3e/4
59    //   — stacked-expert MoE GGUF goes through Box<dyn StackedExpertGgufLinear<Self>>
60    //   returned by `load_quant_experts`.
61    // - `type GptqStore` (Marlin/dequant GPTQ storage) was removed in Phase C
62    //   step 4e — stacked-expert Marlin MoE goes through
63    //   Arc<dyn MarlinExpertStack<Self>> returned by `load_gptq_stacked`,
64    //   and single-tensor GPTQ goes through Box<dyn Linear<Self>> returned
65    //   by `load_gptq`. Adding a new Marlin-capable backend is purely a
66    //   new MarlinExpertStack<NewBackend> impl — no Backend trait edits.
67
68    /// Create a new execution context (begin accumulating work).
69    fn new_context() -> Self::Context;
70
71    /// Run `body` while binding context-free backend operations to an
72    /// explicit device ordinal when the backend supports multi-device scopes.
73    ///
74    /// Most backends have no per-ordinal concept and use the default no-op
75    /// implementation. CUDA overrides this once its stream/context caches are
76    /// device-keyed, allowing layer-split stages to load and execute on their
77    /// selected GPU instead of relying on process-global defaults.
78    fn with_device_ordinal<R>(_device_ordinal: Option<usize>, body: impl FnOnce() -> R) -> R {
79        body()
80    }
81
82    /// Whether [`Self::with_device_ordinal`] actually switches backend
83    /// execution to the requested ordinal.
84    fn supports_device_ordinal_scope() -> bool {
85        false
86    }
87
88    /// Flush accumulated work and wait for completion.
89    /// CPU: no-op. Metal: commit + waitUntilCompleted. CUDA: stream sync.
90    fn sync(ctx: &mut Self::Context);
91
92    /// Whether the backend context is currently inside a graph-capture window.
93    ///
94    /// Synchronizing a CUDA stream while capture is active raises
95    /// `CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED`; diagnostic probes that time
96    /// sub-ops with explicit sync boundaries must skip those boundaries while
97    /// this returns true. Backends without graph capture use the default.
98    fn graph_capture_in_flight(_ctx: &Self::Context) -> bool {
99        false
100    }
101
102    /// Prepare pending GPU work for a following host readback.
103    ///
104    /// Most backends either execute eagerly or synchronize as part of their
105    /// device-to-host copy. Metal shared-buffer reads use the CPU pointer
106    /// directly, so Metal must flush its command buffer before `to_vec`.
107    fn sync_before_host_readback(_ctx: &mut Self::Context) {}
108
109    /// Byte width of buffers returned by [`Self::alloc`].
110    ///
111    /// CUDA activation scratch is fp16, while Metal and CPU scratch are fp32.
112    /// Generic model code uses this for byte offsets into batched scratch
113    /// buffers without checking concrete backend types.
114    fn activation_elem_size_bytes() -> usize {
115        std::mem::size_of::<half::f16>()
116    }
117
118    /// Whether `LlamaFamilyModel::decode_batch_internal` may use its optimized
119    /// batched decode path on this backend.
120    ///
121    /// Backends that do not yet produce correct follow-up logits under
122    /// concurrent dense decode should override this to force the per-item
123    /// fallback until the optimized path is fixed.
124    fn supports_llama_family_batched_decode() -> bool {
125        true
126    }
127
128    /// Whether this backend implements the fused per-item batched Q/K/V
129    /// normalization and RoPE kernel used by the Llama-family batched decode
130    /// path.
131    fn supports_qk_norm_rope_batched_per_item() -> bool {
132        false
133    }
134
135    /// Whether this backend implements batched KV-cache append across multiple
136    /// independent per-request caches.
137    fn supports_kv_cache_append_batched_per_cache() -> bool {
138        false
139    }
140
141    /// Whether this backend implements batched decode attention across multiple
142    /// independent per-request caches.
143    fn supports_flash_attention_batched_per_cache() -> bool {
144        false
145    }
146
147    // Graph capability moved to the `BackendGraph` supertrait at the end
148    // of this file. CUDA implements its overrides; Metal/CPU inherit
149    // unsupported defaults via empty `impl BackendGraph for X {}` blocks.
150
151    // ── GPTQ (INT4 quantization) ────────────────────────────────────────
152    //
153    // Two-step: load (once per weight) → gemm (per forward). The store
154    // holds whatever backend-specific format is fastest; caller code
155    // (GptqLinear) is dtype-agnostic.
156
157    /// Zero the first `len` elements of a Self::Buffer. CUDA path uses
158    /// cuMemsetD16Async; default returns unsupported.
159    fn zero_buffer(_ctx: &mut Self::Context, _buf: &mut Self::Buffer, _len: usize) -> Result<()> {
160        Err(FerrumError::unsupported(
161            "zero_buffer not implemented for this backend",
162        ))
163    }
164
165    /// Phase D step 2+3: unified typed allocator. Replaces per-dtype
166    /// `alloc_u32` / `alloc_typed_i32` / etc. The buffer is dtype-
167    /// tagged at the wrapper level (`CudaBuf::U32`, `MetalBuf` with
168    /// `Dtype::U32`, `CpuBuf::U32`), so reads/writes through `.as_<T>()`
169    /// accessors get the correct byte count automatically.
170    fn alloc_typed(dtype: super::Dtype, n: usize) -> Self::Buffer;
171
172    /// Upload typed host data — replaces `from_slice_i32` /
173    /// `from_slice_u32` etc. The host element type `T` carries its
174    /// `Dtype` via the `HostDtype` marker so dispatch in the impl
175    /// is a one-line `match T::DTYPE`.
176    fn from_slice_typed<T: super::HostDtype>(data: &[T]) -> Self::Buffer;
177
178    /// In-place typed write — replaces `write_u32` / `write_i32_into`
179    /// / `write_f32_into`. The buffer must already be dtype-tagged
180    /// matching `T::DTYPE` (typically alloc'd via `alloc_typed` or
181    /// `from_slice_typed`).
182    fn write_typed<T: super::HostDtype>(
183        ctx: &mut Self::Context,
184        dst: &mut Self::Buffer,
185        data: &[T],
186    );
187
188    // ── GEMM ────────────────────────────────────────────────────────────
189
190    fn gemm(
191        ctx: &mut Self::Context,
192        a: &Self::Buffer,
193        b: &Self::Buffer,
194        out: &mut Self::Buffer,
195        m: usize,
196        n: usize,
197        k: usize,
198    );
199
200    // ── Norms ───────────────────────────────────────────────────────────
201
202    fn rms_norm(
203        ctx: &mut Self::Context,
204        x: &Self::Buffer,
205        w: &Self::Buffer,
206        eps: f32,
207        out: &mut Self::Buffer,
208        tokens: usize,
209        dim: usize,
210    );
211
212    fn fused_add_rms_norm(
213        ctx: &mut Self::Context,
214        residual: &mut Self::Buffer,
215        x: &Self::Buffer,
216        w: &Self::Buffer,
217        eps: f32,
218        out: &mut Self::Buffer,
219        tokens: usize,
220        dim: usize,
221    );
222
223    // ── Attention ───────────────────────────────────────────────────────
224
225    fn flash_attention(
226        ctx: &mut Self::Context,
227        q: &Self::Buffer,
228        k: &Self::Buffer,
229        v: &Self::Buffer,
230        out: &mut Self::Buffer,
231        batch: usize,
232        q_len: usize,
233        kv_len: usize,
234        pos_offset: usize,
235        cfg: &AttnConfig,
236    );
237
238    /// Multi-Head Latent Attention — DeepSeek V2 / V3's compressed-KV
239    /// attention variant. Extension point only; no backend implements it
240    /// yet. DeepSeek V3 landing in Phase D/E will fill this in.
241    ///
242    /// `q`: full Q `[batch, num_heads, q_len, head_dim]`
243    /// `kv_compressed`: latent KV `[batch, kv_len, kv_lora_rank]`
244    /// `kv_rope`: per-position rope-applied key heads `[batch, kv_len, qk_rope_head_dim]`
245    /// `out`: `[batch, num_heads, q_len, head_dim]`
246    #[allow(clippy::too_many_arguments)]
247    fn mla_attention(
248        _ctx: &mut Self::Context,
249        _q: &Self::Buffer,
250        _kv_compressed: &Self::Buffer,
251        _kv_rope: &Self::Buffer,
252        _out: &mut Self::Buffer,
253        _batch: usize,
254        _q_len: usize,
255        _kv_len: usize,
256        _pos_offset: usize,
257        _cfg: &AttnConfig,
258        _kv_lora_rank: usize,
259        _qk_rope_head_dim: usize,
260    ) -> Result<()> {
261        Err(FerrumError::unsupported(
262            "mla_attention not implemented for this backend; required by \
263             DeepSeek V2/V3 (Phase D/E)",
264        ))
265    }
266
267    /// Recurrent gated DeltaNet update used by linear-attention layers.
268    ///
269    /// Layouts are token-major:
270    /// - `query` / `key`: `[tokens, key_heads, key_dim]`
271    /// - `value` / `out`: `[tokens, value_heads, value_dim]`
272    /// - `g` / `beta`: `[tokens, value_heads]`
273    /// - `initial_state` / `final_state`: `[value_heads, value_dim, key_dim]`
274    ///
275    /// Backends may require these buffers to be F32. CUDA currently provides
276    /// the native W3 path; unsupported backends should use the model-level
277    /// reference path instead of silently round-tripping through the host.
278    #[allow(clippy::too_many_arguments)]
279    fn recurrent_gated_delta_rule_f32(
280        _ctx: &mut Self::Context,
281        _query: &Self::Buffer,
282        _key: &Self::Buffer,
283        _value: &Self::Buffer,
284        _g: &Self::Buffer,
285        _beta: &Self::Buffer,
286        _initial_state: &Self::Buffer,
287        _out: &mut Self::Buffer,
288        _final_state: &mut Self::Buffer,
289        _tokens: usize,
290        _key_heads: usize,
291        _value_heads: usize,
292        _key_dim: usize,
293        _value_dim: usize,
294        _use_qk_l2norm: bool,
295        _scale: f32,
296    ) -> Result<()> {
297        Err(FerrumError::unsupported(
298            "recurrent_gated_delta_rule_f32 not implemented for this backend",
299        ))
300    }
301
302    /// Batched one-token recurrent gated DeltaNet update.
303    ///
304    /// Layouts are independent-sequence token-major:
305    /// - `query` / `key`: `[batch, key_heads, key_dim]`
306    /// - `value` / `out`: `[batch, value_heads, value_dim]`
307    /// - `g` / `beta`: `[batch, value_heads]`
308    /// - `initial_states` / `final_states`: `[batch, value_heads, value_dim, key_dim]`
309    ///
310    /// This is the decode-time counterpart of
311    /// [`Self::recurrent_gated_delta_rule_f32`] for continuous batching. Each
312    /// batch row has its own recurrent state; there is no temporal dependency
313    /// across rows.
314    #[allow(clippy::too_many_arguments)]
315    fn recurrent_gated_delta_rule_batch_f32(
316        _ctx: &mut Self::Context,
317        _query: &Self::Buffer,
318        _key: &Self::Buffer,
319        _value: &Self::Buffer,
320        _g: &Self::Buffer,
321        _beta: &Self::Buffer,
322        _initial_states: &Self::Buffer,
323        _out: &mut Self::Buffer,
324        _final_states: &mut Self::Buffer,
325        _batch: usize,
326        _key_heads: usize,
327        _value_heads: usize,
328        _key_dim: usize,
329        _value_dim: usize,
330        _use_qk_l2norm: bool,
331        _scale: f32,
332    ) -> Result<()> {
333        Err(FerrumError::unsupported(
334            "recurrent_gated_delta_rule_batch_f32 not implemented for this backend",
335        ))
336    }
337
338    /// Whether this backend can update Qwen3.5 decode-time recurrent state
339    /// directly from a persistent slot-indexed state slab.
340    fn supports_qwen35_indexed_recurrent_state() -> bool {
341        false
342    }
343
344    /// Persistent state-slab dtype supported by the fast indexed Qwen3.5 GDN
345    /// kernels. Activation/cache dtype alone is not sufficient: each backend
346    /// must report the dtype its indexed conv and DeltaNet kernels can update
347    /// directly.
348    fn qwen35_indexed_recurrent_state_dtype() -> Dtype {
349        Dtype::F32
350    }
351
352    /// Whether this backend can consume Qwen3.5 GDN decode projections in the
353    /// vLLM-packed layout:
354    /// - `in_proj_qkvz`: `[q, k, v, z]`
355    /// - `in_proj_ba`: `[b, a]`
356    ///
357    /// This avoids two small decode projection launches and lets the prepare
358    /// kernel split the packed outputs while updating indexed recurrent state.
359    fn supports_qwen35_packed_gdn_decode_prepare() -> bool {
360        false
361    }
362
363    /// Whether this backend can consume Qwen3.5 GDN prefill projections in the
364    /// vLLM-packed layout:
365    /// - `in_proj_qkvz`: `[q, k, v, z]`
366    /// - `in_proj_ba`: `[b, a]`
367    ///
368    /// This avoids two projection launches on chunked/varlen prefill and lets
369    /// the prepare kernel split the packed outputs while doing causal conv.
370    fn supports_qwen35_packed_gdn_prefill_prepare() -> bool {
371        false
372    }
373
374    /// Whether this backend can keep Qwen3.5 packed GDN decode projections
375    /// packed through the recurrent update, without splitting q/k/v/g/beta
376    /// into intermediate buffers.
377    fn supports_qwen35_packed_gdn_recurrent_decode() -> bool {
378        false
379    }
380
381    /// Batched one-token recurrent gated DeltaNet update over a persistent
382    /// slot-indexed state slab.
383    ///
384    /// Layouts:
385    /// - `query` / `key`: `[batch, key_heads, key_dim]`
386    /// - `value` / `out`: `[batch, value_heads, value_dim]`
387    /// - `g` / `beta`: `[batch, value_heads]`
388    /// - `state_slots`: `[max_slots, value_heads, value_dim, key_dim]`
389    /// - `slot_indices`: `[batch]` u32 indices into `state_slots`
390    ///
391    /// Each row reads and updates the state slot selected by `slot_indices[row]`.
392    #[allow(clippy::too_many_arguments)]
393    fn recurrent_gated_delta_rule_batch_indexed_f32(
394        _ctx: &mut Self::Context,
395        _query: &Self::Buffer,
396        _key: &Self::Buffer,
397        _value: &Self::Buffer,
398        _g: &Self::Buffer,
399        _beta: &Self::Buffer,
400        _state_slots: &mut Self::Buffer,
401        _slot_indices: &Self::Buffer,
402        _out: &mut Self::Buffer,
403        _batch: usize,
404        _max_slots: usize,
405        _key_heads: usize,
406        _value_heads: usize,
407        _key_dim: usize,
408        _value_dim: usize,
409        _use_qk_l2norm: bool,
410        _scale: f32,
411    ) -> Result<()> {
412        Err(FerrumError::unsupported(
413            "recurrent_gated_delta_rule_batch_indexed_f32 not implemented for this backend",
414        ))
415    }
416
417    /// Batched one-token recurrent gated DeltaNet update directly from packed
418    /// decode-time q/k/v and raw b/a projections.
419    ///
420    /// Layouts:
421    /// - `mixed_qkv`: `[batch, q, k, v]` where q/k are
422    ///   `[key_heads, key_dim]` and v is `[value_heads, value_dim]`
423    /// - `ba_raw`: `[batch, b, a]` with each half `[value_heads]`
424    /// - `state_slots`: `[max_slots, value_heads, value_dim, key_dim]`
425    /// - `slot_indices`: `[batch]` u32 indices into `state_slots`
426    #[allow(clippy::too_many_arguments)]
427    fn recurrent_gated_delta_rule_batch_indexed_packed_f32(
428        _ctx: &mut Self::Context,
429        _mixed_qkv: &Self::Buffer,
430        _ba_raw: &Self::Buffer,
431        _a_log: &Self::Buffer,
432        _dt_bias: &Self::Buffer,
433        _state_slots: &mut Self::Buffer,
434        _slot_indices: &Self::Buffer,
435        _out: &mut Self::Buffer,
436        _batch: usize,
437        _max_slots: usize,
438        _key_heads: usize,
439        _value_heads: usize,
440        _key_dim: usize,
441        _value_dim: usize,
442        _scale: f32,
443    ) -> Result<()> {
444        Err(FerrumError::unsupported(
445            "recurrent_gated_delta_rule_batch_indexed_packed_f32 not implemented for this backend",
446        ))
447    }
448
449    /// Variable-length batched recurrent gated DeltaNet prefill update.
450    ///
451    /// Layouts are token-major over all concatenated prefill chunks:
452    /// - `query` / `key`: `[total_tokens, key_heads, key_dim]`
453    /// - `value` / `out`: `[total_tokens, value_heads, value_dim]`
454    /// - `g` / `beta`: `[total_tokens, value_heads]`
455    /// - `cu_seqlens`: `[batch + 1]` u32 prefix sum into the flat token axis
456    /// - `initial_states` / `final_states`: `[batch, value_heads, value_dim, key_dim]`
457    ///
458    /// Each sequence advances independently from its own initial recurrent
459    /// state and writes one final state. This is the prefill counterpart of
460    /// [`Self::recurrent_gated_delta_rule_batch_f32`] and matches the
461    /// `cu_seqlens` shape used by vLLM-style chunked GDN prefill.
462    #[allow(clippy::too_many_arguments)]
463    fn recurrent_gated_delta_rule_varlen_f32(
464        _ctx: &mut Self::Context,
465        _query: &Self::Buffer,
466        _key: &Self::Buffer,
467        _value: &Self::Buffer,
468        _g: &Self::Buffer,
469        _beta: &Self::Buffer,
470        _initial_states: &Self::Buffer,
471        _cu_seqlens: &Self::Buffer,
472        _out: &mut Self::Buffer,
473        _final_states: &mut Self::Buffer,
474        _batch: usize,
475        _total_tokens: usize,
476        _key_heads: usize,
477        _value_heads: usize,
478        _key_dim: usize,
479        _value_dim: usize,
480        _use_qk_l2norm: bool,
481        _scale: f32,
482    ) -> Result<()> {
483        Err(FerrumError::unsupported(
484            "recurrent_gated_delta_rule_varlen_f32 not implemented for this backend",
485        ))
486    }
487
488    /// Prepare a gated-Delta linear-attention block:
489    /// depthwise causal conv + SiLU over `mixed_qkv_raw`, split into Q/K/V,
490    /// and compute GDN gates `g` and `beta`.
491    #[allow(clippy::too_many_arguments)]
492    fn linear_attention_prepare_f32(
493        _ctx: &mut Self::Context,
494        _mixed_qkv_raw: &Self::Buffer,
495        _conv_weight: &Self::Buffer,
496        _a_raw: &Self::Buffer,
497        _b_raw: &Self::Buffer,
498        _a_log: &Self::Buffer,
499        _dt_bias: &Self::Buffer,
500        _query: &mut Self::Buffer,
501        _key: &mut Self::Buffer,
502        _value: &mut Self::Buffer,
503        _g: &mut Self::Buffer,
504        _beta: &mut Self::Buffer,
505        _tokens: usize,
506        _key_heads: usize,
507        _value_heads: usize,
508        _key_dim: usize,
509        _value_dim: usize,
510        _conv_kernel: usize,
511        _apply_qk_l2norm: bool,
512    ) -> Result<()> {
513        Err(FerrumError::unsupported(
514            "linear_attention_prepare_f32 not implemented for this backend",
515        ))
516    }
517
518    /// Varlen prefill-time gated-Delta linear-attention preparation.
519    ///
520    /// This is the batched/stateful counterpart of
521    /// [`Self::linear_attention_prepare_f32`]:
522    /// - `mixed_qkv_raw`: `[total_tokens, conv_channels]`
523    /// - `a_raw` / `b_raw` / `g` / `beta`: `[total_tokens, value_heads]`
524    /// - `query` / `key`: `[total_tokens, key_heads, key_dim]`
525    /// - `value`: `[total_tokens, value_heads, value_dim]`
526    /// - `cu_seqlens`: `[batch + 1]` u32 prefix sum into the flat token axis
527    /// - `token_seq_indices`: `[total_tokens]` u32 sequence row per flat token
528    /// - `initial_conv_states` / `final_conv_states`:
529    ///   `[batch, conv_channels, conv_kernel - 1]`
530    ///
531    /// Each sequence's depthwise causal conv reads only that sequence plus its
532    /// own initial conv state and writes one final conv state. That boundary
533    /// handling is required before a varlen recurrent GDN pass can be used for
534    /// product prefill batching.
535    #[allow(clippy::too_many_arguments)]
536    fn linear_attention_prepare_varlen_f32(
537        _ctx: &mut Self::Context,
538        _mixed_qkv_raw: &Self::Buffer,
539        _conv_weight: &Self::Buffer,
540        _initial_conv_states: &Self::Buffer,
541        _a_raw: &Self::Buffer,
542        _b_raw: &Self::Buffer,
543        _a_log: &Self::Buffer,
544        _dt_bias: &Self::Buffer,
545        _cu_seqlens: &Self::Buffer,
546        _token_seq_indices: &Self::Buffer,
547        _query: &mut Self::Buffer,
548        _key: &mut Self::Buffer,
549        _value: &mut Self::Buffer,
550        _g: &mut Self::Buffer,
551        _beta: &mut Self::Buffer,
552        _final_conv_states: &mut Self::Buffer,
553        _batch: usize,
554        _total_tokens: usize,
555        _key_heads: usize,
556        _value_heads: usize,
557        _key_dim: usize,
558        _value_dim: usize,
559        _conv_kernel: usize,
560        _apply_qk_l2norm: bool,
561    ) -> Result<()> {
562        Err(FerrumError::unsupported(
563            "linear_attention_prepare_varlen_f32 not implemented for this backend",
564        ))
565    }
566
567    /// Varlen prefill-time gated-Delta linear-attention preparation from
568    /// vLLM-packed Qwen3.5 projections.
569    ///
570    /// Layouts:
571    /// - `mixed_qkvz_raw`: `[total_tokens, q, k, v, z]`
572    /// - `ba_raw`: `[total_tokens, b, a]`
573    /// - `z`: `[total_tokens, value_heads, value_dim]`
574    /// Other outputs and state layouts match [`Self::linear_attention_prepare_varlen_f32`].
575    #[allow(clippy::too_many_arguments)]
576    fn linear_attention_prepare_varlen_packed_qkvz_ba_f32(
577        _ctx: &mut Self::Context,
578        _mixed_qkvz_raw: &Self::Buffer,
579        _ba_raw: &Self::Buffer,
580        _conv_weight: &Self::Buffer,
581        _initial_conv_states: &Self::Buffer,
582        _a_log: &Self::Buffer,
583        _dt_bias: &Self::Buffer,
584        _cu_seqlens: &Self::Buffer,
585        _token_seq_indices: &Self::Buffer,
586        _query: &mut Self::Buffer,
587        _key: &mut Self::Buffer,
588        _value: &mut Self::Buffer,
589        _z: &mut Self::Buffer,
590        _g: &mut Self::Buffer,
591        _beta: &mut Self::Buffer,
592        _final_conv_states: &mut Self::Buffer,
593        _batch: usize,
594        _total_tokens: usize,
595        _key_heads: usize,
596        _value_heads: usize,
597        _key_dim: usize,
598        _value_dim: usize,
599        _conv_kernel: usize,
600        _apply_qk_l2norm: bool,
601    ) -> Result<()> {
602        Err(FerrumError::unsupported(
603            "linear_attention_prepare_varlen_packed_qkvz_ba_f32 not implemented for this backend",
604        ))
605    }
606
607    /// Decode-time gated-Delta linear-attention preparation for one token.
608    ///
609    /// This is the stateful counterpart of [`Self::linear_attention_prepare_f32`]:
610    /// it reads `[conv_channels, conv_kernel - 1]` causal-conv state, appends the
611    /// current raw QKV token, writes the next conv state, then emits Q/K/V and
612    /// GDN gates for the current token. The layout mirrors vLLM's Qwen GDN
613    /// `conv_state` + temporal-state split.
614    #[allow(clippy::too_many_arguments)]
615    fn linear_attention_decode_prepare_f32(
616        _ctx: &mut Self::Context,
617        _mixed_qkv_raw: &Self::Buffer,
618        _conv_weight: &Self::Buffer,
619        _conv_state: &Self::Buffer,
620        _a_raw: &Self::Buffer,
621        _b_raw: &Self::Buffer,
622        _a_log: &Self::Buffer,
623        _dt_bias: &Self::Buffer,
624        _query: &mut Self::Buffer,
625        _key: &mut Self::Buffer,
626        _value: &mut Self::Buffer,
627        _g: &mut Self::Buffer,
628        _beta: &mut Self::Buffer,
629        _next_conv_state: &mut Self::Buffer,
630        _key_heads: usize,
631        _value_heads: usize,
632        _key_dim: usize,
633        _value_dim: usize,
634        _conv_kernel: usize,
635        _apply_qk_l2norm: bool,
636    ) -> Result<()> {
637        Err(FerrumError::unsupported(
638            "linear_attention_decode_prepare_f32 not implemented for this backend",
639        ))
640    }
641
642    /// Batched stateful one-token linear-attention preparation.
643    ///
644    /// This processes `batch` independent decode rows:
645    /// - `mixed_qkv_raw`: `[batch, conv_channels]`
646    /// - `conv_states` / `next_conv_states`: `[batch, conv_channels, conv_kernel - 1]`
647    /// - `a_raw` / `b_raw` / `g` / `beta`: `[batch, value_heads]`
648    /// - `query` / `key`: `[batch, key_heads, key_dim]`
649    /// - `value`: `[batch, value_heads, value_dim]`
650    #[allow(clippy::too_many_arguments)]
651    fn linear_attention_decode_prepare_batch_f32(
652        _ctx: &mut Self::Context,
653        _mixed_qkv_raw: &Self::Buffer,
654        _conv_weight: &Self::Buffer,
655        _conv_states: &Self::Buffer,
656        _a_raw: &Self::Buffer,
657        _b_raw: &Self::Buffer,
658        _a_log: &Self::Buffer,
659        _dt_bias: &Self::Buffer,
660        _query: &mut Self::Buffer,
661        _key: &mut Self::Buffer,
662        _value: &mut Self::Buffer,
663        _g: &mut Self::Buffer,
664        _beta: &mut Self::Buffer,
665        _next_conv_states: &mut Self::Buffer,
666        _batch: usize,
667        _key_heads: usize,
668        _value_heads: usize,
669        _key_dim: usize,
670        _value_dim: usize,
671        _conv_kernel: usize,
672        _apply_qk_l2norm: bool,
673    ) -> Result<()> {
674        Err(FerrumError::unsupported(
675            "linear_attention_decode_prepare_batch_f32 not implemented for this backend",
676        ))
677    }
678
679    /// Batched stateful one-token linear-attention preparation over a
680    /// persistent slot-indexed conv-state slab.
681    ///
682    /// `conv_state_slots` has layout `[max_slots, conv_channels, conv_kernel-1]`
683    /// and is updated in place at `slot_indices[row]`.
684    #[allow(clippy::too_many_arguments)]
685    fn linear_attention_decode_prepare_batch_indexed_f32(
686        _ctx: &mut Self::Context,
687        _mixed_qkv_raw: &Self::Buffer,
688        _conv_weight: &Self::Buffer,
689        _conv_state_slots: &mut Self::Buffer,
690        _slot_indices: &Self::Buffer,
691        _a_raw: &Self::Buffer,
692        _b_raw: &Self::Buffer,
693        _a_log: &Self::Buffer,
694        _dt_bias: &Self::Buffer,
695        _query: &mut Self::Buffer,
696        _key: &mut Self::Buffer,
697        _value: &mut Self::Buffer,
698        _g: &mut Self::Buffer,
699        _beta: &mut Self::Buffer,
700        _batch: usize,
701        _max_slots: usize,
702        _key_heads: usize,
703        _value_heads: usize,
704        _key_dim: usize,
705        _value_dim: usize,
706        _conv_kernel: usize,
707        _apply_qk_l2norm: bool,
708    ) -> Result<()> {
709        Err(FerrumError::unsupported(
710            "linear_attention_decode_prepare_batch_indexed_f32 not implemented for this backend",
711        ))
712    }
713
714    #[allow(clippy::too_many_arguments)]
715    fn linear_attention_decode_prepare_batch_indexed_packed_qkvz_ba_f32(
716        _ctx: &mut Self::Context,
717        _mixed_qkvz_raw: &Self::Buffer,
718        _ba_raw: &Self::Buffer,
719        _conv_weight: &Self::Buffer,
720        _conv_state_slots: &mut Self::Buffer,
721        _slot_indices: &Self::Buffer,
722        _a_log: &Self::Buffer,
723        _dt_bias: &Self::Buffer,
724        _query: &mut Self::Buffer,
725        _key: &mut Self::Buffer,
726        _value: &mut Self::Buffer,
727        _z: &mut Self::Buffer,
728        _g: &mut Self::Buffer,
729        _beta: &mut Self::Buffer,
730        _batch: usize,
731        _max_slots: usize,
732        _key_heads: usize,
733        _value_heads: usize,
734        _key_dim: usize,
735        _value_dim: usize,
736        _conv_kernel: usize,
737        _apply_qk_l2norm: bool,
738    ) -> Result<()> {
739        Err(FerrumError::unsupported(
740            "linear_attention_decode_prepare_batch_indexed_packed_qkvz_ba_f32 not implemented for this backend",
741        ))
742    }
743
744    #[allow(clippy::too_many_arguments)]
745    fn linear_attention_decode_prepare_batch_indexed_packed_qkvz_to_mixed_f32(
746        _ctx: &mut Self::Context,
747        _mixed_qkvz_raw: &Self::Buffer,
748        _conv_weight: &Self::Buffer,
749        _conv_state_slots: &mut Self::Buffer,
750        _slot_indices: &Self::Buffer,
751        _mixed_qkv: &mut Self::Buffer,
752        _z: &mut Self::Buffer,
753        _batch: usize,
754        _max_slots: usize,
755        _key_heads: usize,
756        _value_heads: usize,
757        _key_dim: usize,
758        _value_dim: usize,
759        _conv_kernel: usize,
760    ) -> Result<()> {
761        Err(FerrumError::unsupported(
762            "linear_attention_decode_prepare_batch_indexed_packed_qkvz_to_mixed_f32 not implemented for this backend",
763        ))
764    }
765
766    /// Gated RMSNorm used after recurrent DeltaNet core:
767    /// `out = rms_norm(core) * weight * silu(z)`.
768    #[allow(clippy::too_many_arguments)]
769    fn gated_rms_norm_f32(
770        _ctx: &mut Self::Context,
771        _core: &Self::Buffer,
772        _z: &Self::Buffer,
773        _weight: &Self::Buffer,
774        _out: &mut Self::Buffer,
775        _tokens: usize,
776        _heads: usize,
777        _dim: usize,
778        _eps: f32,
779    ) -> Result<()> {
780        Err(FerrumError::unsupported(
781            "gated_rms_norm_f32 not implemented for this backend",
782        ))
783    }
784
785    // ── Element-wise ────────────────────────────────────────────────────
786    //
787    // Models use `add_inplace` for residual updates and `copy_slice` for the
788    // row-extraction step in prefill. Offset-free copy / non-inplace add are
789    // not needed by the current Model-as-Code path; they can return later if
790    // a model actually requires them.
791
792    /// Copy `len` floats from `src[src_offset..]` to `dst[dst_offset..]`.
793    ///
794    /// Needed for Qwen3Model::prefill to pluck the last token's hidden state
795    /// out of `residual[seq_len, h]` without round-tripping through host RAM.
796    /// `Backend::copy` is the offset-free variant; `copy_slice` additionally
797    /// supports non-zero source and destination offsets.
798    fn copy_slice(
799        ctx: &mut Self::Context,
800        src: &Self::Buffer,
801        src_offset: usize,
802        dst: &mut Self::Buffer,
803        dst_offset: usize,
804        len: usize,
805    );
806
807    // ── Embedding ───────────────────────────────────────────────────────
808
809    fn embedding_lookup(
810        ctx: &mut Self::Context,
811        table: &Self::Buffer,
812        ids: &[u32],
813        out: &mut Self::Buffer,
814        dim: usize,
815    );
816
817    /// Device-buffer variant of `embedding_lookup` for graph-capturable
818    /// MoE routing — the gather step before phase-1 GEMM in
819    /// `moe_forward_bucketed`. The host-slice `embedding_lookup` does
820    /// `clone_htod(ids)` internally, which records stale host pointers
821    /// under CUDA Graph capture replay.
822    ///
823    /// `ids: &Self::Buffer` must be a device I32 buffer of `batch`
824    /// elements (e.g. `Qwen3MoeScratch::route_packed_idx_dev`).
825    /// `batch` is passed explicitly since a typed CudaBuf carries
826    /// its element count but the caller often wants a partial gather.
827    ///
828    /// Default impl: round-trip via `to_vec` + dispatch the host-slice
829    /// variant. CUDA overrides.
830    fn embedding_lookup_dev(
831        ctx: &mut Self::Context,
832        table: &Self::Buffer,
833        ids: &Self::Buffer,
834        out: &mut Self::Buffer,
835        batch: usize,
836        dim: usize,
837    ) {
838        // Default: round-trip. CUDA overrides with a direct device-arg
839        // kernel launch (no clone_htod).
840        let ids_host_f32 = Self::to_vec(ids, batch);
841        let ids_host_u32: Vec<u32> = ids_host_f32.iter().map(|x| x.to_bits()).collect();
842        Self::embedding_lookup(ctx, table, &ids_host_u32, out, dim);
843    }
844
845    // ── Transformer-specific fused ops ─────────────────────────────────
846    // These avoid CPU round-trips for data layout transformations.
847
848    /// Split fused QKV [tokens, q_dim+2*kv_dim] into separate Q, K, V buffers.
849    /// Q: [tokens, q_dim], K: [tokens, kv_dim], V: [tokens, kv_dim]
850    fn split_qkv(
851        ctx: &mut Self::Context,
852        qkv: &Self::Buffer,
853        q: &mut Self::Buffer,
854        k: &mut Self::Buffer,
855        v: &mut Self::Buffer,
856        tokens: usize,
857        q_dim: usize,
858        kv_dim: usize,
859    );
860
861    /// Split fused gate_up [tokens, 2*im] into gate [tokens, im] and up [tokens, im],
862    /// then compute SiLU(gate) * up → out [tokens, im].
863    fn fused_silu_mul_split(
864        ctx: &mut Self::Context,
865        gate_up: &Self::Buffer,
866        out: &mut Self::Buffer,
867        tokens: usize,
868        im: usize,
869    );
870
871    /// GeGLU variant of [`Backend::fused_silu_mul_split`]:
872    /// gelu_tanh(gate) * up → out. Matches HF `gelu_pytorch_tanh`
873    /// (Gemma family MLP). Panics by default: wire a real kernel on any
874    /// backend that loads a GeGLU model.
875    fn fused_gelu_tanh_mul_split(
876        _ctx: &mut Self::Context,
877        _gate_up: &Self::Buffer,
878        _out: &mut Self::Buffer,
879        _tokens: usize,
880        _im: usize,
881    ) {
882        panic!("fused_gelu_tanh_mul_split not implemented for this backend");
883    }
884
885    /// `buf[i] *= scale` over the first `len` elements. Gemma-family
886    /// embedding scaling (×√hidden_size on residual-stream entry).
887    /// Default round-trips through host memory — correct but slow;
888    /// override on backends that serve Gemma models.
889    fn scale_inplace(ctx: &mut Self::Context, buf: &mut Self::Buffer, scale: f32, len: usize) {
890        Self::sync(ctx);
891        let mut v = Self::to_vec(buf, len);
892        for x in v.iter_mut() {
893            *x *= scale;
894        }
895        *buf = Self::from_slice(&v);
896    }
897
898    /// Fused QK-norm + RoPE + transpose-to-head-major.
899    ///
900    /// `mode` selects the operation:
901    ///   0 = transpose only (typical for V, which needs no norm and no RoPE)
902    ///   1 = per-head RMS norm + RoPE + transpose  (Q/K with QK-norm, Qwen3)
903    ///   2 = RoPE + transpose                       (Q/K without QK-norm, Llama/Mistral)
904    ///
905    /// input:   `[tokens, heads, head_dim]`  (token-major, output of split_qkv)
906    /// output:  `[heads, tokens, head_dim]`  (head-major, ready for flash_attn / kv_cache_append)
907    ///
908    /// `pos_offset` is the position of token 0 (decode uses current seq len;
909    /// prefill uses 0). Within the batch, positions are taken as `pos_offset + i`.
910    ///
911    /// This is the primary attention-input preparation op. Backends that have a
912    /// fused kernel (Metal's `qk_norm_rope_transpose_f32`) will be dramatically
913    /// faster than composing norm + rope + transpose separately; the CPU
914    /// fallback lowers to the individual ops.
915    #[allow(clippy::too_many_arguments)]
916    fn qk_norm_rope(
917        ctx: &mut Self::Context,
918        input: &Self::Buffer,
919        norm_w: &Self::Buffer,
920        cos: &Self::Buffer,
921        sin: &Self::Buffer,
922        output: &mut Self::Buffer,
923        tokens: usize,
924        heads: usize,
925        head_dim: usize,
926        pos_offset: usize,
927        eps: f32,
928        mode: i32,
929    );
930
931    /// Q/K preparation variant for Qwen3.5 full attention.
932    ///
933    /// `input_stride` is the per-token feature width in `input`, and
934    /// `input_offset` is the first feature for this projection inside each
935    /// token row. This lets Q read the first `num_heads * head_dim` slice from
936    /// Qwen3.5's gated `q_proj` output while leaving the attention gate slice
937    /// in place for a later device-side post-op.
938    ///
939    /// `rope_dim` may be smaller than `head_dim`; dimensions outside
940    /// `rope_dim` are normalized and copied but not rotated. `mode` follows
941    /// [`Backend::qk_norm_rope`]: 0 transpose only, 1 RMSNorm+RoPE, 2 RoPE
942    /// only, 3 RMSNorm+interleaved RoPE for Qwen3.5's mrope layout.
943    #[allow(clippy::too_many_arguments)]
944    fn qk_norm_rope_partial(
945        ctx: &mut Self::Context,
946        input: &Self::Buffer,
947        norm_w: &Self::Buffer,
948        cos: &Self::Buffer,
949        sin: &Self::Buffer,
950        output: &mut Self::Buffer,
951        tokens: usize,
952        heads: usize,
953        head_dim: usize,
954        rope_dim: usize,
955        input_stride: usize,
956        input_offset: usize,
957        input_head_stride: usize,
958        pos_offset: usize,
959        eps: f32,
960        mode: i32,
961    ) -> Result<()> {
962        if rope_dim == head_dim
963            && input_stride == heads * head_dim
964            && input_offset == 0
965            && input_head_stride == head_dim
966            && mode != 3
967        {
968            Self::qk_norm_rope(
969                ctx, input, norm_w, cos, sin, output, tokens, heads, head_dim, pos_offset, eps,
970                mode,
971            );
972            return Ok(());
973        }
974        Err(FerrumError::unsupported(
975            "qk_norm_rope_partial not implemented for this backend",
976        ))
977    }
978
979    /// Apply Qwen3.5 attention output gate in place. Gated Qwen3.5 full
980    /// attention stores q_proj rows as per-head `[query, gate]` slices, so
981    /// `context[token, head, dim] *= sigmoid(q_proj[token, head, head_dim + dim])`.
982    ///
983    /// The CUDA implementation is a single device kernel. Backends that do not
984    /// implement it must fail rather than silently copying data through host
985    /// memory on product paths.
986    fn qwen35_apply_attention_gate(
987        _ctx: &mut Self::Context,
988        _context: &mut Self::Buffer,
989        _query_raw: &Self::Buffer,
990        _tokens: usize,
991        _q_total: usize,
992        _q_proj_total: usize,
993        _head_dim: usize,
994    ) -> Result<()> {
995        Err(FerrumError::unsupported(
996            "qwen35_apply_attention_gate not implemented for this backend",
997        ))
998    }
999
1000    /// Apply one scalar gate per token to a token-major hidden buffer:
1001    /// `values[token, dim] *= sigmoid(gate[token])`.
1002    fn qwen35_apply_token_gate(
1003        _ctx: &mut Self::Context,
1004        _values: &mut Self::Buffer,
1005        _gate: &Self::Buffer,
1006        _tokens: usize,
1007        _hidden_size: usize,
1008    ) -> Result<()> {
1009        Err(FerrumError::unsupported(
1010            "qwen35_apply_token_gate not implemented for this backend",
1011        ))
1012    }
1013
1014    /// Apply one scalar gate per token to `values`, then add the gated values
1015    /// into `dst` in place:
1016    /// `values[token, dim] *= sigmoid(gate[token])`;
1017    /// `dst[token, dim] += values[token, dim]`.
1018    ///
1019    /// The default preserves the old two-dispatch behavior. CUDA overrides
1020    /// this for Qwen3.5 sparse-MoE shared-expert decode, where it removes the
1021    /// separate token-gate and merge launches while keeping the gated
1022    /// `values` buffer available for tracing/debugging.
1023    fn qwen35_apply_token_gate_and_add_inplace(
1024        ctx: &mut Self::Context,
1025        dst: &mut Self::Buffer,
1026        values: &mut Self::Buffer,
1027        gate: &Self::Buffer,
1028        tokens: usize,
1029        hidden_size: usize,
1030    ) -> Result<()> {
1031        Self::qwen35_apply_token_gate(ctx, values, gate, tokens, hidden_size)?;
1032        Self::add_inplace(ctx, dst, values, tokens * hidden_size);
1033        Ok(())
1034    }
1035
1036    /// Interleave shared-expert gate/up projections from token-major
1037    /// `[tokens, intermediate]` buffers into `[tokens, 2 * intermediate]`.
1038    ///
1039    /// The default keeps backend behavior unchanged. CUDA overrides this to
1040    /// avoid `2 * tokens` tiny device-to-device copies per MoE layer.
1041    fn qwen35_interleave_gate_up(
1042        ctx: &mut Self::Context,
1043        gate: &Self::Buffer,
1044        up: &Self::Buffer,
1045        out: &mut Self::Buffer,
1046        tokens: usize,
1047        intermediate: usize,
1048    ) -> Result<()> {
1049        for token in 0..tokens {
1050            Self::copy_slice(
1051                ctx,
1052                gate,
1053                token * intermediate,
1054                out,
1055                token * 2 * intermediate,
1056                intermediate,
1057            );
1058            Self::copy_slice(
1059                ctx,
1060                up,
1061                token * intermediate,
1062                out,
1063                token * 2 * intermediate + intermediate,
1064                intermediate,
1065            );
1066        }
1067        Ok(())
1068    }
1069
1070    /// Batched kv_cache_append across M caches in one launch. Each item
1071    /// writes its (head-major) K-or-V row into its own cache at offset
1072    /// read from `cache_lens[i]`. Replaces M sequential
1073    /// `kv_cache_append_head_major` calls with a single dispatch.
1074    ///
1075    /// `new_data` layout: `[m, nkv, hd]` item-major (each item's slice
1076    /// is contiguous, identical to the `k/v_normed_batched` produced by
1077    /// `qk_norm_rope_batched_per_item`).
1078    /// `caches`: per-cache `[nkv, capacity, hd]` head-major.
1079    /// `cache_lens`: device buffer (u32 storage, length ≥ m). Caller
1080    /// fills via `B::write_u32_into` BEFORE the call. Required for
1081    /// CUDA-graph capture: the kernel reads from this stable device
1082    /// buffer, so a captured graph can be replayed with new lens by
1083    /// just rewriting the buffer between launches.
1084    fn kv_cache_append_batched_per_cache(
1085        _ctx: &mut Self::Context,
1086        _caches: &[&Self::Buffer],
1087        _new_data: &Self::Buffer,
1088        _cache_lens: &Self::Buffer,
1089        _capacity: usize,
1090        _m: usize,
1091        _nkv: usize,
1092        _hd: usize,
1093        _slot: usize,
1094    ) -> Result<()> {
1095        Err(FerrumError::unsupported(
1096            "kv_cache_append_batched_per_cache not implemented for this backend",
1097        ))
1098    }
1099
1100    /// Batched flash_attention across M decode caches in one launch.
1101    /// Replaces the per-item `flash_attention(q_len=1, ...)` × M
1102    /// loop in the non-paged batched-decode path.
1103    ///
1104    /// API takes Vec<&Buffer> for the per-cache K/V buffers (each
1105    /// `[nkv, capacity, hd]` head-major) plus host-side `kv_lens`.
1106    /// Backends that implement it must extract per-cache device
1107    /// pointers, build the device arrays the kernel needs, and launch
1108    /// one kernel covering all M items.
1109    ///
1110    /// `q` layout: [m, nq, hd] item-major (matches the
1111    /// `qk_norm_rope_batched_per_item` output for q_len=1).
1112    /// `out` layout: [m, nq, hd] item-major — written directly into
1113    /// the caller's batched attn_out buffer, no per-item copy needed.
1114    ///
1115    /// CUDA-only for now (kernel `batched_decode_attention` exists in
1116    /// `kernels/batched_decode_attention.cu`).
1117    /// `kv_lens`: device buffer (u32 storage, length ≥ m) — same
1118    /// design as `kv_cache_append_batched_per_cache::cache_lens`.
1119    /// `sliding_window`: common decode window for every item; `0` means
1120    /// full causal attention, `w > 0` means each item attends only to the
1121    /// last `w` valid KV positions.
1122    fn flash_attention_batched_per_cache(
1123        _ctx: &mut Self::Context,
1124        _q: &Self::Buffer,
1125        _k_caches: &[&Self::Buffer],
1126        _v_caches: &[&Self::Buffer],
1127        _kv_lens: &Self::Buffer,
1128        _out: &mut Self::Buffer,
1129        _nq: usize,
1130        _nkv: usize,
1131        _hd: usize,
1132        _scale: f32,
1133        _max_valid_kv: usize,
1134        _capacity: usize,
1135        _sliding_window: usize,
1136        _slot: usize,
1137    ) -> Result<()> {
1138        Err(FerrumError::unsupported(
1139            "flash_attention_batched_per_cache not implemented for this backend",
1140        ))
1141    }
1142
1143    /// Batched per-item-position variant of `qk_norm_rope` for the
1144    /// non-paged batched-decode path. Each of the `m` items has its own
1145    /// absolute RoPE position (read from a device i32 buffer of length
1146    /// `m`). Layout is item-major in *both* input and output:
1147    ///
1148    ///   input  [m, heads, head_dim]
1149    ///   output [m, heads, head_dim]   (no head-major transpose)
1150    ///
1151    /// Item-major output keeps the per-item flash_attention slice
1152    /// contiguous (`output[i * heads * head_dim ..]` is item i's whole
1153    /// Q tensor in head-major-equivalent layout for q_len=1).
1154    ///
1155    /// Replaces the M sequential single-item launches in the existing
1156    /// `forward_layer_batched_decode` path with one batched dispatch.
1157    /// CUDA-only for now; other backends fall through to the default
1158    /// `unsupported` and the caller falls back to the per-item loop.
1159    fn qk_norm_rope_batched_per_item(
1160        _ctx: &mut Self::Context,
1161        _input: &Self::Buffer,
1162        _norm_w: &Self::Buffer,
1163        _cos: &Self::Buffer,
1164        _sin: &Self::Buffer,
1165        _output: &mut Self::Buffer,
1166        _positions: &Self::Buffer,
1167        _m: usize,
1168        _heads: usize,
1169        _head_dim: usize,
1170        _eps: f32,
1171        _mode: i32,
1172    ) -> Result<()> {
1173        Err(FerrumError::unsupported(
1174            "qk_norm_rope_batched_per_item not implemented for this backend",
1175        ))
1176    }
1177
1178    /// Fused split-QKV + QK-norm + RoPE + head-major transpose.
1179    ///
1180    /// Single-dispatch replacement for the (`split_qkv` → 3× `qk_norm_rope`)
1181    /// chain on the decode-attention prelude. Reads the linear-layer
1182    /// fused-QKV output once and writes head-major Q/K/V directly into
1183    /// attention scratch.
1184    ///
1185    /// `qkv` layout: `[tokens, q_heads*hd + 2*kv_heads*hd]`.
1186    /// `q_out`: `[q_heads, tokens, hd]`. `k_out`/`v_out`: `[kv_heads, tokens, hd]`.
1187    /// `qk_mode`: 1 = norm + half-split RoPE for Q/K (Qwen3 with QK-norm),
1188    ///            2 = half-split RoPE only for Q/K,
1189    ///            3 = interleaved RoPE only for Q/K (GGUF LLaMA / llama.cpp layout).
1190    /// V always falls through to transpose-only.
1191    ///
1192    /// Default returns Unsupported. Backends that implement it are
1193    /// expected to be dramatically faster than the four-dispatch chain.
1194    #[allow(clippy::too_many_arguments)]
1195    fn split_qkv_norm_rope(
1196        _ctx: &mut Self::Context,
1197        _qkv: &Self::Buffer,
1198        _q_norm_w: &Self::Buffer,
1199        _k_norm_w: &Self::Buffer,
1200        _cos: &Self::Buffer,
1201        _sin: &Self::Buffer,
1202        _q_out: &mut Self::Buffer,
1203        _k_out: &mut Self::Buffer,
1204        _v_out: &mut Self::Buffer,
1205        _tokens: usize,
1206        _q_heads: usize,
1207        _kv_heads: usize,
1208        _head_dim: usize,
1209        _pos_offset: usize,
1210        _eps: f32,
1211        _qk_mode: i32,
1212    ) -> Result<()> {
1213        Err(FerrumError::unsupported(
1214            "split_qkv_norm_rope not implemented for this backend",
1215        ))
1216    }
1217
1218    /// Variant of [`Backend::split_qkv_norm_rope`] that writes the new
1219    /// K and V directly into pre-allocated head-major KV cache buffers
1220    /// at slot `[kv_heads, cache_len .. cache_len + tokens, hd]`.
1221    /// Eliminates the trailing `kv_cache_append_head_major` dispatch on
1222    /// the decode hot path. Q still lands in per-token head-major
1223    /// scratch (flash-attention reads it as the query).
1224    ///
1225    /// Default returns Unsupported. Backends without the fused kernel
1226    /// can keep using `split_qkv_norm_rope` + `kv_cache_append_head_major`.
1227    #[allow(clippy::too_many_arguments)]
1228    fn split_qkv_norm_rope_into_cache(
1229        _ctx: &mut Self::Context,
1230        _qkv: &Self::Buffer,
1231        _q_norm_w: &Self::Buffer,
1232        _k_norm_w: &Self::Buffer,
1233        _cos: &Self::Buffer,
1234        _sin: &Self::Buffer,
1235        _q_out: &mut Self::Buffer,
1236        _cache_k: &mut Self::Buffer,
1237        _cache_v: &mut Self::Buffer,
1238        _tokens: usize,
1239        _q_heads: usize,
1240        _kv_heads: usize,
1241        _head_dim: usize,
1242        _pos_offset: usize,
1243        _eps: f32,
1244        _qk_mode: i32,
1245        _cache_len: usize,
1246        _cache_capacity: usize,
1247    ) -> Result<()> {
1248        Err(FerrumError::unsupported(
1249            "split_qkv_norm_rope_into_cache not implemented for this backend",
1250        ))
1251    }
1252
1253    // Phase D step 2: alloc_u32 / write_u32 deleted. Callers use the
1254    // unified `alloc_typed(Dtype::U32, n)` + `write_typed(&[u32])` API
1255    // declared above.
1256
1257    /// Append new K/V into a pre-allocated head-major cache buffer.
1258    ///
1259    /// `cache_k` / `cache_v`: `[nkv, capacity, hd]` (head-major, pre-allocated)
1260    /// `new_k_head_major` / `new_v_head_major`: `[nkv, new_tokens, hd]`
1261    ///   — produced directly by `qk_norm_rope`, no extra transpose needed.
1262    ///
1263    /// In-place append at slot `[nkv, cache_len..cache_len+new_tokens, hd]`.
1264    /// Caller owns `cache_len` bookkeeping.
1265    #[allow(clippy::too_many_arguments)]
1266    fn kv_cache_append_head_major(
1267        ctx: &mut Self::Context,
1268        cache_k: &mut Self::Buffer,
1269        cache_v: &mut Self::Buffer,
1270        cache_len: usize,
1271        cache_capacity: usize,
1272        new_k_head_major: &Self::Buffer,
1273        new_v_head_major: &Self::Buffer,
1274        new_tokens: usize,
1275        nkv: usize,
1276        hd: usize,
1277    );
1278
1279    /// Transpose [heads, tokens, dim] → [tokens, heads, dim].
1280    /// Called after `flash_attention` to restore token-major layout for O-proj.
1281    fn transpose_head_to_token(
1282        ctx: &mut Self::Context,
1283        src: &Self::Buffer,
1284        dst: &mut Self::Buffer,
1285        tokens: usize,
1286        heads: usize,
1287        dim: usize,
1288    );
1289
1290    /// Inverse of `transpose_head_to_token`: [tokens, heads, dim] →
1291    /// [heads, tokens, dim]. Used by the CUDA `paged_decode_attention`
1292    /// wrapper to convert `paged_varlen_attention`'s token-major output
1293    /// back to the head-major layout that Qwen3MoeModel expects.
1294    /// Default panics — backends without a paged-KV CUDA path don't
1295    /// hit this code.
1296    fn transpose_token_to_head(
1297        _ctx: &mut Self::Context,
1298        _src: &Self::Buffer,
1299        _dst: &mut Self::Buffer,
1300        _tokens: usize,
1301        _heads: usize,
1302        _dim: usize,
1303    ) {
1304        panic!("transpose_token_to_head not implemented for this backend");
1305    }
1306
1307    /// residual[i] += x[i] (in-place)
1308    fn add_inplace(
1309        ctx: &mut Self::Context,
1310        residual: &mut Self::Buffer,
1311        x: &Self::Buffer,
1312        len: usize,
1313    );
1314
1315    /// `dst[i] += scale * src[i]` — scalar-broadcast scaled add, in place.
1316    ///
1317    /// MoE per-token combine writes `out[b] += weight_k * expert_k(x[b])`
1318    /// for each top-K expert; this primitive is the per-call accumulate.
1319    /// Backends without a dedicated kernel can fall back to the default
1320    /// implementation, which round-trips through host memory — correct,
1321    /// but slow on a hot path. Override on any backend you actually
1322    /// dispatch MoE on.
1323    fn scaled_add_inplace(
1324        _ctx: &mut Self::Context,
1325        dst: &mut Self::Buffer,
1326        src: &Self::Buffer,
1327        scale: f32,
1328        len: usize,
1329    ) {
1330        let mut dst_v = Self::to_vec(dst, len);
1331        let src_v = Self::to_vec(src, len);
1332        for i in 0..len {
1333            dst_v[i] += scale * src_v[i];
1334        }
1335        // Move the new buffer into the slot pointed to by `dst`. Safe
1336        // because `Self::Buffer: Send + Sync` and the old buffer is
1337        // dropped here when overwritten.
1338        *dst = Self::from_slice(&dst_v);
1339    }
1340
1341    /// Strided variant of [`Backend::fused_silu_mul_split`] for the
1342    /// bucketed MoE path: reads `gate_up` rows starting at
1343    /// `in_row_offset`, writes `out` rows starting at `out_row_offset`.
1344    #[allow(clippy::too_many_arguments)]
1345    fn fused_silu_mul_split_strided(
1346        _ctx: &mut Self::Context,
1347        _gate_up: &Self::Buffer,
1348        _in_row_offset: usize,
1349        _out: &mut Self::Buffer,
1350        _out_row_offset: usize,
1351        _tokens: usize,
1352        _intermediate: usize,
1353    ) {
1354        unimplemented!("fused_silu_mul_split_strided default impl missing");
1355    }
1356
1357    /// Broadcast bias add: `data[r, c] += bias[c]` for every row.
1358    /// Required by Bert / Clip / Whisper whose linear projections carry a bias.
1359    fn add_bias(
1360        ctx: &mut Self::Context,
1361        data: &mut Self::Buffer,
1362        bias: &Self::Buffer,
1363        rows: usize,
1364        cols: usize,
1365    );
1366
1367    /// Full LayerNorm (mean + variance normalisation + affine), distinct from
1368    /// the `rms_norm` used by Llama-family decoders.
1369    ///   `out[r, c] = ((x[r, c] - mean) / sqrt(var + eps)) * gamma[c] + beta[c]`
1370    /// Where `mean` and `var` are reduced over the last dim (cols).
1371    #[allow(clippy::too_many_arguments)]
1372    fn layer_norm(
1373        ctx: &mut Self::Context,
1374        x: &Self::Buffer,
1375        gamma: &Self::Buffer,
1376        beta: &Self::Buffer,
1377        eps: f32,
1378        out: &mut Self::Buffer,
1379        tokens: usize,
1380        dim: usize,
1381    );
1382
1383    /// Element-wise GELU activation (erf-based, matches PyTorch default).
1384    fn gelu(ctx: &mut Self::Context, x: &Self::Buffer, out: &mut Self::Buffer, len: usize);
1385
1386    // ── Buffer management (context-free) ────────────────────────────────
1387
1388    fn alloc(len: usize) -> Self::Buffer;
1389    fn to_vec(buf: &Self::Buffer, len: usize) -> Vec<f32>;
1390    fn from_slice(data: &[f32]) -> Self::Buffer;
1391
1392    fn write_f32_to_activation(ctx: &mut Self::Context, dst: &mut Self::Buffer, data: &[f32]) {
1393        if data.is_empty() {
1394            return;
1395        }
1396        let src = Self::from_slice(data);
1397        Self::copy_slice(ctx, &src, 0, dst, 0, data.len());
1398    }
1399
1400    /// Convert a typed F32 device buffer into the backend activation dtype.
1401    ///
1402    /// CUDA activations are FP16 for tensor-core/Marlin kernels, while the
1403    /// Qwen3.5 gated-Delta core keeps recurrent math in F32. Backends with
1404    /// non-F32 activations should override this with a device-side conversion.
1405    fn f32_to_activation(
1406        ctx: &mut Self::Context,
1407        input_f32: &Self::Buffer,
1408        out: &mut Self::Buffer,
1409        len: usize,
1410    ) {
1411        Self::sync(ctx);
1412        let values = Self::to_vec(input_f32, len);
1413        Self::write_f32_to_activation(ctx, out, &values);
1414    }
1415
1416    /// Whether this backend can keep Gemma-style sandwich residuals in a
1417    /// device-side F32 shadow while continuing to feed FP16 activations into
1418    /// projection kernels. The default is false so existing CPU/Metal paths
1419    /// keep their current host-side fallback behavior.
1420    fn supports_device_f32_residual_shadow() -> bool {
1421        false
1422    }
1423
1424    /// Copy an activation buffer into a typed F32 shadow buffer.
1425    fn activation_to_f32_shadow(
1426        ctx: &mut Self::Context,
1427        src: &Self::Buffer,
1428        dst_f32: &mut Self::Buffer,
1429        len: usize,
1430    ) {
1431        let data = Self::to_vec(src, len);
1432        Self::write_typed::<f32>(ctx, dst_f32, &data);
1433    }
1434
1435    /// Add an activation buffer directly into an existing F32 residual shadow.
1436    ///
1437    /// `scratch_f32` is provided for portable fallback implementations. CUDA can
1438    /// fuse the activation-to-F32 conversion and residual add into one kernel.
1439    fn activation_add_to_f32_shadow(
1440        ctx: &mut Self::Context,
1441        src: &Self::Buffer,
1442        residual_f32: &mut Self::Buffer,
1443        scratch_f32: &mut Self::Buffer,
1444        len: usize,
1445    ) {
1446        Self::activation_to_f32_shadow(ctx, src, scratch_f32, len);
1447        Self::add_inplace(ctx, residual_f32, scratch_f32, len);
1448    }
1449
1450    /// RMSNorm an activation buffer and write the result into a typed F32
1451    /// scratch buffer. Used for Gemma post-attn/post-ffn branch norms.
1452    fn rms_norm_activation_to_f32(
1453        ctx: &mut Self::Context,
1454        input: &Self::Buffer,
1455        weight: &Self::Buffer,
1456        eps: f32,
1457        out_f32: &mut Self::Buffer,
1458        tokens: usize,
1459        dim: usize,
1460    ) {
1461        let input_h = Self::to_vec(input, tokens * dim);
1462        let weight_h = Self::to_vec(weight, dim);
1463        let mut out = vec![0.0f32; tokens * dim];
1464        for row in 0..tokens {
1465            let offset = row * dim;
1466            let mut variance = 0.0f32;
1467            for i in 0..dim {
1468                let x = input_h[offset + i];
1469                variance += x * x;
1470            }
1471            let inv_rms = (variance / dim as f32 + eps).sqrt().recip();
1472            for i in 0..dim {
1473                out[offset + i] = input_h[offset + i] * inv_rms * weight_h[i];
1474            }
1475        }
1476        Self::write_typed::<f32>(ctx, out_f32, &out);
1477    }
1478
1479    /// RMSNorm an activation buffer and add the F32 result directly into an
1480    /// existing F32 residual shadow. `scratch_f32` is provided for backend
1481    /// fallbacks that need to materialize the normalized branch.
1482    fn rms_norm_activation_add_to_f32(
1483        ctx: &mut Self::Context,
1484        input: &Self::Buffer,
1485        weight: &Self::Buffer,
1486        eps: f32,
1487        residual_f32: &mut Self::Buffer,
1488        scratch_f32: &mut Self::Buffer,
1489        tokens: usize,
1490        dim: usize,
1491    ) {
1492        Self::rms_norm_activation_to_f32(ctx, input, weight, eps, scratch_f32, tokens, dim);
1493        Self::add_inplace(ctx, residual_f32, scratch_f32, tokens * dim);
1494    }
1495
1496    /// RMSNorm a typed F32 shadow buffer and write the normalized result back
1497    /// to the backend's regular activation dtype.
1498    fn rms_norm_f32_to_activation(
1499        ctx: &mut Self::Context,
1500        input_f32: &Self::Buffer,
1501        weight: &Self::Buffer,
1502        eps: f32,
1503        out: &mut Self::Buffer,
1504        tokens: usize,
1505        dim: usize,
1506    ) {
1507        let input_h = Self::to_vec(input_f32, tokens * dim);
1508        let weight_h = Self::to_vec(weight, dim);
1509        let mut normed = vec![0.0f32; tokens * dim];
1510        for row in 0..tokens {
1511            let offset = row * dim;
1512            let mut variance = 0.0f32;
1513            for i in 0..dim {
1514                let x = input_h[offset + i];
1515                variance += x * x;
1516            }
1517            let inv_rms = (variance / dim as f32 + eps).sqrt().recip();
1518            for i in 0..dim {
1519                normed[offset + i] = input_h[offset + i] * inv_rms * weight_h[i];
1520            }
1521        }
1522        Self::write_f32_to_activation(ctx, out, &normed);
1523    }
1524
1525    /// Greedy-decode fast path: GPU argmax over each row of a
1526    /// `[m, n]` FP16 logits buffer, returning the m token indices on the
1527    /// host. Saves `m × n × 2` bytes of D2H per call (e.g. 19.5 MB at
1528    /// c=32, vocab=152064) and the host-side argmax scan (~150 µs × m).
1529    ///
1530    /// Default impl falls back to the slow path: full `to_vec` + host
1531    /// argmax. CUDA overrides with a native kernel + tiny D2H (m × 4 B).
1532    /// Backends that don't override pay the same cost as
1533    /// `to_vec` + host argmax, so callers can call this unconditionally.
1534    fn argmax_rows_f16(
1535        _ctx: &mut Self::Context,
1536        logits: &Self::Buffer,
1537        m: usize,
1538        n: usize,
1539    ) -> Result<Vec<u32>> {
1540        let host = Self::to_vec(logits, m * n);
1541        let mut out = Vec::with_capacity(m);
1542        for row in 0..m {
1543            let slice = &host[row * n..(row + 1) * n];
1544            let mut max_idx = 0usize;
1545            let mut max_val = f32::NEG_INFINITY;
1546            for (i, &v) in slice.iter().enumerate() {
1547                if v > max_val {
1548                    max_val = v;
1549                    max_idx = i;
1550                }
1551            }
1552            out.push(max_idx as u32);
1553        }
1554        Ok(out)
1555    }
1556
1557    fn argmax_rows_f16_masked(
1558        _ctx: &mut Self::Context,
1559        _logits: &Self::Buffer,
1560        _valid_token_mask: &Self::Buffer,
1561        _mask_len: usize,
1562        _m: usize,
1563        _n: usize,
1564    ) -> Result<Vec<u32>> {
1565        Err(FerrumError::unsupported(
1566            "masked GPU argmax is not implemented for this backend",
1567        ))
1568    }
1569
1570    /// Whether this backend can apply a per-row sparse repetition penalty and
1571    /// select the greedy token without reading full logits back to the host.
1572    fn supports_argmax_rows_f16_sparse_repetition_penalty() -> bool {
1573        false
1574    }
1575
1576    /// Greedy-decode fast path with sparse repetition penalty.
1577    ///
1578    /// `row_offsets` has length `m + 1` and indexes into `token_ids`; row `r`
1579    /// owns `token_ids[row_offsets[r]..row_offsets[r + 1]]`. Backends should
1580    /// apply each row's `repetition_penalties[r]` to those logits in-place,
1581    /// then run raw or masked argmax and return one token id per row.
1582    #[allow(clippy::too_many_arguments)]
1583    fn argmax_rows_f16_sparse_repetition_penalty(
1584        _ctx: &mut Self::Context,
1585        _logits: &mut Self::Buffer,
1586        _valid_token_mask: Option<(&Self::Buffer, usize)>,
1587        _row_offsets: &Self::Buffer,
1588        _token_ids: &Self::Buffer,
1589        _repetition_penalties: &Self::Buffer,
1590        _total_token_ids: usize,
1591        _m: usize,
1592        _n: usize,
1593    ) -> Result<Vec<u32>> {
1594        Err(FerrumError::unsupported(
1595            "sparse repetition-penalty GPU argmax is not implemented for this backend",
1596        ))
1597    }
1598
1599    /// Load a weight tensor straight from its on-disk byte representation,
1600    /// letting the backend pick its preferred storage dtype.
1601    ///
1602    /// Default impl upcasts bf16/f16 to f32 via an intermediate Vec, matching
1603    /// pre-existing loader behaviour. Backends override this to go straight
1604    /// from raw bytes into a native half-precision buffer (e.g. Metal with
1605    /// `FERRUM_METAL_DTYPE=f16`), avoiding the transient 2× RAM spike.
1606    fn from_weight_bytes(raw: &[u8], src_dtype: SrcDtype) -> Self::Buffer {
1607        let data = src_dtype.to_f32_vec(raw);
1608        Self::from_slice(&data)
1609    }
1610
1611    // (The Phase A3 unified `gemm_quant(QuantWeights, QuantKind)` stub
1612    // that used to live here is superseded by the `load_quant` /
1613    // `gemm_quant(QuantStore)` pair earlier in this trait — same idea,
1614    // but the store hides the per-kind buffer layout so callers don't
1615    // have to construct a per-kind `QuantWeights<'_, Self>` packet.)
1616}
1617
1618// ════════════════════════════════════════════════════════════════════════
1619// BackendPagedKv capability (vLLM-style paged KV cache + paged attention)
1620// ════════════════════════════════════════════════════════════════════════
1621//
1622// Paged KV pool with block-table indirection, plus the paged attention
1623// kernel variants that read through that indirection. CUDA + Metal both
1624// implement the real kernels; CPU `impl BackendPagedKv for CpuBackend {}`
1625// inherits unsupported defaults.
1626
1627/// Capability-trait for backends that support paged KV cache + paged attention.
1628pub trait BackendPagedKv: Backend {
1629    /// Whether this backend has a paged-KV decode path
1630    /// (`paged_decode_attention` etc.). Currently true for Metal, false
1631    /// for CPU. Used to decide the default of `FERRUM_METAL_PAGED_KV` —
1632    /// the `serve` path should opt in automatically when supported so
1633    /// users get the bench-quality concurrent-decode numbers without
1634    /// having to learn the flag.
1635    fn supports_paged_kv() -> bool {
1636        false
1637    }
1638    /// Pre-populate the per-slot device-pointer scratch arrays used by
1639    /// the batched kernels (`kv_cache_append_batched_per_cache` and
1640    /// `flash_attention_batched_per_cache`). Required by the CUDA-graph
1641    /// capture path: the captured graph contains only kernel launches
1642    /// (no captured `memcpy_htod`), so the device scratch must be fresh
1643    /// when the graph replays.
1644    ///
1645    /// Caller passes flat layer-major slices: `k_caches[li * m + i]` and
1646    /// `v_caches[li * m + i]`. Backend extracts each cache's device
1647    /// pointer and writes into its corresponding slot in the device
1648    /// scratch via SYNCHRONOUS memcpy (not captured by stream capture).
1649    ///
1650    /// CUDA-only; other backends fall through to the default
1651    /// `unsupported` and the caller skips the population call.
1652    fn populate_batched_pointers(
1653        _ctx: &mut Self::Context,
1654        _k_caches: &[&Self::Buffer],
1655        _v_caches: &[&Self::Buffer],
1656        _num_layers: usize,
1657        _m: usize,
1658    ) -> Result<()> {
1659        Err(FerrumError::unsupported(
1660            "populate_batched_pointers not implemented for this backend",
1661        ))
1662    }
1663    /// Paged-KV variant of [`Self::split_qkv_norm_rope_into_cache`].
1664    ///
1665    /// Same fused split + qk-norm + RoPE, but K/V are written into a
1666    /// paged pool `[num_blocks, kv_heads, block_size, head_dim]`
1667    /// indexed via `block_table[logical_block]` → physical_block.
1668    /// Q still goes to head-major scratch.
1669    ///
1670    /// Default returns Unsupported. Backends that lack a paged kernel
1671    /// keep using the contiguous variant.
1672    /// `qkv_byte_offset` / `q_out_byte_offset` let the caller pass a
1673    /// slice of a larger batched buffer (used by the multi-seq paged
1674    /// path in `decode_batch_internal`). For single-seq dispatch they
1675    /// should be 0.
1676    #[allow(clippy::too_many_arguments)]
1677    fn split_qkv_norm_rope_into_paged_cache(
1678        _ctx: &mut Self::Context,
1679        _qkv: &Self::Buffer,
1680        _qkv_byte_offset: u64,
1681        _q_norm_w: &Self::Buffer,
1682        _k_norm_w: &Self::Buffer,
1683        _cos: &Self::Buffer,
1684        _sin: &Self::Buffer,
1685        _q_out: &mut Self::Buffer,
1686        _q_out_byte_offset: u64,
1687        _cache_k: &mut Self::Buffer,
1688        _cache_v: &mut Self::Buffer,
1689        _block_table: &Self::Buffer,
1690        _tokens: usize,
1691        _q_heads: usize,
1692        _kv_heads: usize,
1693        _head_dim: usize,
1694        _pos_offset: usize,
1695        _eps: f32,
1696        _qk_mode: i32,
1697        _cache_len: usize,
1698        _block_size: usize,
1699        _max_num_blocks_per_seq: usize,
1700    ) -> Result<()> {
1701        Err(FerrumError::unsupported(
1702            "split_qkv_norm_rope_into_paged_cache not implemented for this backend",
1703        ))
1704    }
1705    /// Paged-KV variant of [`Self::flash_attention`].
1706    ///
1707    /// Decode (`q_len == 1`):
1708    ///   `q`/`out`: `[num_seqs, num_heads, head_dim]` (token-major)
1709    ///
1710    /// Causal prefill (`q_len > 1`, single seq):
1711    ///   `q`/`out`: `[num_heads, q_len, head_dim]` (head-major — the
1712    ///              layout produced by `split_qkv_norm_rope_into_paged_cache`)
1713    ///   The kernel applies a per-q-token causal mask using
1714    ///   `context_lens[seq]` as the FINAL kv_len (= `pos_offset + q_len`):
1715    ///   token i sees positions `[0, context_lens - q_len + 1 + i)`.
1716    ///
1717    /// Common to both:
1718    ///   `k_pool`/`v_pool`: `[num_blocks, num_kv_heads, block_size, head_dim]`
1719    ///   `block_tables`: `[num_seqs, max_num_blocks_per_seq]` u32
1720    ///   `context_lens`: `[num_seqs]` u32
1721    ///
1722    /// Backends without a paged kernel return Unsupported; callers are
1723    /// expected to fall back to contiguous KV.
1724    #[allow(clippy::too_many_arguments)]
1725    fn paged_decode_attention(
1726        _ctx: &mut Self::Context,
1727        _q: &Self::Buffer,
1728        _k_pool: &Self::Buffer,
1729        _v_pool: &Self::Buffer,
1730        _out: &mut Self::Buffer,
1731        _block_tables: &Self::Buffer,
1732        _context_lens: &Self::Buffer,
1733        _num_seqs: usize,
1734        _num_heads: usize,
1735        _num_kv_heads: usize,
1736        _head_dim: usize,
1737        _block_size: usize,
1738        _max_num_blocks_per_seq: usize,
1739        _q_len: usize,
1740    ) -> Result<()> {
1741        Err(FerrumError::unsupported(
1742            "paged_decode_attention not implemented for this backend",
1743        ))
1744    }
1745    /// Capability: does this backend implement
1746    /// `split_qkv_norm_rope_into_paged_cache_varlen` and
1747    /// `paged_varlen_attention`? Required by the unified mixed-batch
1748    /// forward path used by `LlamaFamilyModel::unified_forward`. Default
1749    /// false; backends that ship the varlen kernels override.
1750    fn supports_varlen_qkv() -> bool {
1751        false
1752    }
1753    /// Varlen variant of [`Self::split_qkv_norm_rope_into_paged_cache`].
1754    ///
1755    /// Single launch covering ALL sequences in the batch. Reads
1756    /// `pos_offsets[seq]`, `cu_seqlens_q[seq]`, and the per-seq
1757    /// block_table from device buffers — graph-capturable (the per-iter
1758    /// state is in buffers, not kernel scalars). Replaces the per-item
1759    /// dispatch loop in `unified_forward_layer` with one call.
1760    ///
1761    /// Layouts:
1762    /// - `qkv`: `[m_total, q_dim + 2 * kv_dim]` token-major
1763    /// - `q_out`: `[m_total, q_heads, head_dim]` token-major (matches
1764    ///   what `paged_varlen_attention` reads)
1765    /// - `cache_k` / `cache_v`: paged pool same as `paged_varlen_attention`
1766    /// - `cu_seqlens_q`: `[num_seqs + 1]` u32 prefix sum
1767    /// - `pos_offsets`: `[num_seqs]` u32, starting kv_pos per seq
1768    /// - `block_tables`: `[num_seqs, max_blocks_per_seq]` i32 stacked
1769    #[allow(clippy::too_many_arguments)]
1770    fn split_qkv_norm_rope_into_paged_cache_varlen(
1771        _ctx: &mut Self::Context,
1772        _qkv: &Self::Buffer,
1773        _q_norm_w: &Self::Buffer,
1774        _k_norm_w: &Self::Buffer,
1775        _cos: &Self::Buffer,
1776        _sin: &Self::Buffer,
1777        _q_out: &mut Self::Buffer,
1778        _cache_k: &mut Self::Buffer,
1779        _cache_v: &mut Self::Buffer,
1780        _cu_seqlens_q: &Self::Buffer,
1781        _pos_offsets: &Self::Buffer,
1782        _block_tables: &Self::Buffer,
1783        _num_seqs: usize,
1784        _m_total: usize,
1785        _q_heads: usize,
1786        _kv_heads: usize,
1787        _head_dim: usize,
1788        _eps: f32,
1789        _qk_mode: i32,
1790        _block_size: usize,
1791        _max_blocks_per_seq: usize,
1792    ) -> Result<()> {
1793        Err(FerrumError::unsupported(
1794            "split_qkv_norm_rope_into_paged_cache_varlen not implemented for this backend",
1795        ))
1796    }
1797    /// Variable-length paged attention with GQA + causal mask.
1798    ///
1799    /// Supports a unified mixed batch where each sequence contributes
1800    /// 1 (decode) or N (prefill chunk) query tokens — the workhorse for
1801    /// chunked-prefill. See `kernels/paged_varlen_attention.cu` for the
1802    /// kernel itself.
1803    ///
1804    /// Layouts:
1805    /// - `q` / `out`: `[total_q_tokens, num_heads, head_dim]` (token-
1806    ///   major, FP16). `total_q_tokens` = `cu_seqlens_q[num_seqs]`.
1807    /// - `k_pool` / `v_pool`: paged block pool, layout matches
1808    ///   `paged_decode_attention`.
1809    /// - `cu_seqlens_q`: `[num_seqs + 1]` u32 prefix sum, with
1810    ///   `cu_seqlens_q[0] = 0` and `cu_seqlens_q[num_seqs] = total_q_tokens`.
1811    /// - `pos_offsets`: `[num_seqs]` u32, the starting absolute KV
1812    ///   position of each seq's first q token (= prior `kv_len`).
1813    /// - `block_tables`: `[num_seqs, max_num_blocks_per_seq]` i32 grid.
1814    ///
1815    /// Each query token attends causally to KV positions
1816    /// `[0, pos_offsets[s] + local_idx]` when `sliding_window == 0`, or
1817    /// only the most recent `sliding_window` positions when non-zero.
1818    #[allow(clippy::too_many_arguments)]
1819    fn paged_varlen_attention(
1820        _ctx: &mut Self::Context,
1821        _q: &Self::Buffer,
1822        _k_pool: &Self::Buffer,
1823        _v_pool: &Self::Buffer,
1824        _out: &mut Self::Buffer,
1825        _cu_seqlens_q: &Self::Buffer,
1826        _pos_offsets: &Self::Buffer,
1827        _block_tables: &Self::Buffer,
1828        _num_seqs: usize,
1829        _total_q_tokens: usize,
1830        _max_kv_len: usize,
1831        _num_heads: usize,
1832        _num_kv_heads: usize,
1833        _head_dim: usize,
1834        _sliding_window: usize,
1835        _block_size: usize,
1836        _max_num_blocks_per_seq: usize,
1837    ) -> Result<()> {
1838        Err(FerrumError::unsupported(
1839            "paged_varlen_attention not implemented for this backend",
1840        ))
1841    }
1842
1843    /// Opt-in vLLM FlashAttention-2 FFI path for FA-layout paged KV.
1844    ///
1845    /// This is intentionally separate from [`Self::paged_varlen_attention`]:
1846    /// it needs the final per-sequence KV lengths (`seq_lens`) and an explicit
1847    /// LSE scratch buffer because the external FA2 runner writes softmax LSE.
1848    /// Default returns Err(unsupported); CUDA overrides when a runtime shim is
1849    /// provided via `FERRUM_FA2_DIRECT_FFI_SHIM`.
1850    #[allow(clippy::too_many_arguments)]
1851    fn paged_varlen_attention_fa2_ffi(
1852        _ctx: &mut Self::Context,
1853        _q: &Self::Buffer,
1854        _k_pool: &Self::Buffer,
1855        _v_pool: &Self::Buffer,
1856        _out: &mut Self::Buffer,
1857        _lse: &mut Self::Buffer,
1858        _cu_seqlens_q: &Self::Buffer,
1859        _seq_lens: &Self::Buffer,
1860        _block_tables: &Self::Buffer,
1861        _num_seqs: usize,
1862        _total_q_tokens: usize,
1863        _max_q_len: usize,
1864        _max_kv_len: usize,
1865        _num_heads: usize,
1866        _num_kv_heads: usize,
1867        _head_dim: usize,
1868        _block_size: usize,
1869        _max_num_blocks_per_seq: usize,
1870    ) -> Result<()> {
1871        Err(FerrumError::unsupported(
1872            "paged_varlen_attention_fa2_ffi not implemented for this backend",
1873        ))
1874    }
1875
1876    /// Batched paged decode attention — multi-seq, single token per seq.
1877    /// Faster path for the unified_forward layer when m_total == num_seqs
1878    /// (every item is a single-token decode). Skips the cu_seqlens_q
1879    /// linear scan that `paged_varlen_attention` does in the fully-mixed
1880    /// case.
1881    ///
1882    /// Layouts:
1883    ///   q              : [num_seqs, num_q_heads, head_dim]
1884    ///   k_pool/v_pool  : paged pool (same as paged_varlen)
1885    ///   block_tables   : [num_seqs, max_num_blocks_per_seq]
1886    ///   valid_kv_lens  : [num_seqs] — current kv_len per seq
1887    ///   out            : [num_seqs, num_q_heads, head_dim]
1888    ///
1889    /// Default returns Err(unsupported); CUDA backend overrides.
1890    #[allow(clippy::too_many_arguments)]
1891    fn paged_batched_decode_attention(
1892        _ctx: &mut Self::Context,
1893        _q: &Self::Buffer,
1894        _k_pool: &Self::Buffer,
1895        _v_pool: &Self::Buffer,
1896        _out: &mut Self::Buffer,
1897        _block_tables: &Self::Buffer,
1898        _valid_kv_lens: &Self::Buffer,
1899        _num_seqs: usize,
1900        _max_kv_len: usize,
1901        _num_heads: usize,
1902        _num_kv_heads: usize,
1903        _head_dim: usize,
1904        _block_size: usize,
1905        _max_num_blocks_per_seq: usize,
1906    ) -> Result<()> {
1907        Err(FerrumError::unsupported(
1908            "paged_batched_decode_attention not implemented for this backend",
1909        ))
1910    }
1911
1912    /// Capability: backend has vLLM-layout paged KV write kernels and the
1913    /// `paged_attention_v2` decode kernel. Models that opt into this layout
1914    /// at construction time (via `FERRUM_USE_VLLM_PAGED_ATTN=1`) must
1915    /// dispatch ALL paged writes and reads through the `_vllm` variants —
1916    /// the layouts are not compatible. Default `false`.
1917    fn supports_vllm_paged_attn() -> bool {
1918        false
1919    }
1920
1921    /// Qwen3.5 full-attention uses separate q/k/v projections and partial
1922    /// RoPE/gated-Q layout, so it cannot use the fused-QKV paged writer
1923    /// directly. Backends that implement this method can write those
1924    /// separate projections into Ferrum's legacy paged pool consumed by
1925    /// [`Self::paged_batched_decode_attention`].
1926    fn supports_qwen35_paged_qkv() -> bool {
1927        false
1928    }
1929
1930    #[allow(clippy::too_many_arguments)]
1931    fn qwen35_split_qkv_norm_rope_into_paged_cache_varlen(
1932        _ctx: &mut Self::Context,
1933        _query_raw: &Self::Buffer,
1934        _key_raw: &Self::Buffer,
1935        _value_raw: &Self::Buffer,
1936        _q_norm_w: &Self::Buffer,
1937        _k_norm_w: &Self::Buffer,
1938        _cos: &Self::Buffer,
1939        _sin: &Self::Buffer,
1940        _q_out: &mut Self::Buffer,
1941        _cache_k: &mut Self::Buffer,
1942        _cache_v: &mut Self::Buffer,
1943        _cu_seqlens_q: &Self::Buffer,
1944        _token_seq_indices: &Self::Buffer,
1945        _pos_offsets: &Self::Buffer,
1946        _block_tables: &Self::Buffer,
1947        _num_seqs: usize,
1948        _total_q_tokens: usize,
1949        _q_heads: usize,
1950        _kv_heads: usize,
1951        _head_dim: usize,
1952        _rope_dim: usize,
1953        _q_proj_stride: usize,
1954        _q_head_stride: usize,
1955        _kv_proj_stride: usize,
1956        _eps: f32,
1957        _qk_mode: i32,
1958        _block_size: usize,
1959        _max_blocks_per_seq: usize,
1960    ) -> Result<()> {
1961        Err(FerrumError::unsupported(
1962            "qwen35_split_qkv_norm_rope_into_paged_cache_varlen not implemented for this backend",
1963        ))
1964    }
1965
1966    /// vLLM-layout variant of the Qwen3.5 separate q/k/v writer. K/V are
1967    /// written in the layout consumed by [`Self::paged_decode_attention_v2`],
1968    /// while Q remains token-major `[total_q_tokens, q_heads, head_dim]`.
1969    fn supports_qwen35_paged_qkv_vllm() -> bool {
1970        false
1971    }
1972
1973    #[allow(clippy::too_many_arguments)]
1974    fn qwen35_split_qkv_norm_rope_into_paged_cache_varlen_vllm(
1975        _ctx: &mut Self::Context,
1976        _query_raw: &Self::Buffer,
1977        _key_raw: &Self::Buffer,
1978        _value_raw: &Self::Buffer,
1979        _q_norm_w: &Self::Buffer,
1980        _k_norm_w: &Self::Buffer,
1981        _cos: &Self::Buffer,
1982        _sin: &Self::Buffer,
1983        _q_out: &mut Self::Buffer,
1984        _cache_k: &mut Self::Buffer,
1985        _cache_v: &mut Self::Buffer,
1986        _cu_seqlens_q: &Self::Buffer,
1987        _token_seq_indices: &Self::Buffer,
1988        _pos_offsets: &Self::Buffer,
1989        _block_tables: &Self::Buffer,
1990        _num_seqs: usize,
1991        _total_q_tokens: usize,
1992        _q_heads: usize,
1993        _kv_heads: usize,
1994        _head_dim: usize,
1995        _rope_dim: usize,
1996        _q_proj_stride: usize,
1997        _q_head_stride: usize,
1998        _kv_proj_stride: usize,
1999        _eps: f32,
2000        _qk_mode: i32,
2001        _block_size: usize,
2002        _max_blocks_per_seq: usize,
2003    ) -> Result<()> {
2004        Err(FerrumError::unsupported(
2005            "qwen35_split_qkv_norm_rope_into_paged_cache_varlen_vllm not implemented for this backend",
2006        ))
2007    }
2008
2009    /// vLLM-layout variant of
2010    /// [`Self::split_qkv_norm_rope_into_paged_cache`]. K/V are written in
2011    /// vLLM's `paged_attention_v2` layout: K is
2012    /// `[num_blocks, kv_heads, head_dim/x, block_size, x]` (x = 16/sizeof(elem)),
2013    /// V is `[num_blocks, kv_heads, head_dim, block_size]`. Q output and
2014    /// every other argument matches the non-vllm variant exactly so the
2015    /// model layer can swap dispatchers based on a single flag.
2016    #[allow(clippy::too_many_arguments)]
2017    fn split_qkv_norm_rope_into_paged_cache_vllm(
2018        _ctx: &mut Self::Context,
2019        _qkv: &Self::Buffer,
2020        _qkv_byte_offset: u64,
2021        _q_norm_w: &Self::Buffer,
2022        _k_norm_w: &Self::Buffer,
2023        _cos: &Self::Buffer,
2024        _sin: &Self::Buffer,
2025        _q_out: &mut Self::Buffer,
2026        _q_out_byte_offset: u64,
2027        _cache_k: &mut Self::Buffer,
2028        _cache_v: &mut Self::Buffer,
2029        _block_table: &Self::Buffer,
2030        _tokens: usize,
2031        _q_heads: usize,
2032        _kv_heads: usize,
2033        _head_dim: usize,
2034        _pos_offset: usize,
2035        _eps: f32,
2036        _qk_mode: i32,
2037        _cache_len: usize,
2038        _block_size: usize,
2039        _max_num_blocks_per_seq: usize,
2040    ) -> Result<()> {
2041        Err(FerrumError::unsupported(
2042            "split_qkv_norm_rope_into_paged_cache_vllm not implemented for this backend",
2043        ))
2044    }
2045
2046    /// vLLM-layout variant of
2047    /// [`Self::split_qkv_norm_rope_into_paged_cache_varlen`]. Same signature
2048    /// — only the K/V cache layout changes.
2049    #[allow(clippy::too_many_arguments)]
2050    fn split_qkv_norm_rope_into_paged_cache_varlen_vllm(
2051        _ctx: &mut Self::Context,
2052        _qkv: &Self::Buffer,
2053        _q_norm_w: &Self::Buffer,
2054        _k_norm_w: &Self::Buffer,
2055        _cos: &Self::Buffer,
2056        _sin: &Self::Buffer,
2057        _q_out: &mut Self::Buffer,
2058        _cache_k: &mut Self::Buffer,
2059        _cache_v: &mut Self::Buffer,
2060        _cu_seqlens_q: &Self::Buffer,
2061        _pos_offsets: &Self::Buffer,
2062        _block_tables: &Self::Buffer,
2063        _num_seqs: usize,
2064        _m_total: usize,
2065        _q_heads: usize,
2066        _kv_heads: usize,
2067        _head_dim: usize,
2068        _eps: f32,
2069        _qk_mode: i32,
2070        _block_size: usize,
2071        _max_blocks_per_seq: usize,
2072    ) -> Result<()> {
2073        Err(FerrumError::unsupported(
2074            "split_qkv_norm_rope_into_paged_cache_varlen_vllm not implemented for this backend",
2075        ))
2076    }
2077
2078    /// vLLM `paged_attention_v2` — multi-partition split-K decode attention
2079    /// reading the vLLM K/V layout. `q_len` is implicitly 1 (decode only;
2080    /// vLLM's v2 kernel does not support q_len > 1). `max_seq_len` is the
2081    /// max kv_len across the batch — used to size the partition reduction.
2082    #[allow(clippy::too_many_arguments)]
2083    fn paged_decode_attention_v2(
2084        _ctx: &mut Self::Context,
2085        _q: &Self::Buffer,
2086        _k_pool: &Self::Buffer,
2087        _v_pool: &Self::Buffer,
2088        _out: &mut Self::Buffer,
2089        _block_tables: &Self::Buffer,
2090        _context_lens: &Self::Buffer,
2091        _num_seqs: usize,
2092        _num_heads: usize,
2093        _num_kv_heads: usize,
2094        _head_dim: usize,
2095        _block_size: usize,
2096        _max_num_blocks_per_seq: usize,
2097        _max_seq_len: usize,
2098    ) -> Result<()> {
2099        Err(FerrumError::unsupported(
2100            "paged_decode_attention_v2 not implemented for this backend",
2101        ))
2102    }
2103
2104    /// q_len>1 prefill/chunk-prefill attention over vLLM-layout paged KV.
2105    /// This keeps cache layout consistent when `FERRUM_USE_VLLM_PAGED_ATTN=1`
2106    /// and the prompt path writes K/V in the layout consumed later by
2107    /// `paged_decode_attention_v2`.
2108    #[allow(clippy::too_many_arguments)]
2109    fn paged_varlen_attention_vllm_layout(
2110        _ctx: &mut Self::Context,
2111        _q: &Self::Buffer,
2112        _k_pool: &Self::Buffer,
2113        _v_pool: &Self::Buffer,
2114        _out: &mut Self::Buffer,
2115        _block_tables: &Self::Buffer,
2116        _context_lens: &Self::Buffer,
2117        _num_seqs: usize,
2118        _num_heads: usize,
2119        _num_kv_heads: usize,
2120        _head_dim: usize,
2121        _block_size: usize,
2122        _max_num_blocks_per_seq: usize,
2123        _q_len: usize,
2124    ) -> Result<()> {
2125        Err(FerrumError::unsupported(
2126            "paged_varlen_attention_vllm_layout not implemented for this backend",
2127        ))
2128    }
2129
2130    /// Variable-length paged attention over vLLM-layout paged KV.
2131    ///
2132    /// Unlike [`Self::paged_varlen_attention_vllm_layout`], this accepts the
2133    /// same varlen index tensors as [`Self::paged_varlen_attention`] and writes
2134    /// token-major output directly. It is the unified mixed-batch companion for
2135    /// `split_qkv_norm_rope_into_paged_cache_varlen_vllm`.
2136    #[allow(clippy::too_many_arguments)]
2137    fn paged_varlen_attention_vllm(
2138        _ctx: &mut Self::Context,
2139        _q: &Self::Buffer,
2140        _k_pool: &Self::Buffer,
2141        _v_pool: &Self::Buffer,
2142        _out: &mut Self::Buffer,
2143        _cu_seqlens_q: &Self::Buffer,
2144        _pos_offsets: &Self::Buffer,
2145        _block_tables: &Self::Buffer,
2146        _num_seqs: usize,
2147        _total_q_tokens: usize,
2148        _max_kv_len: usize,
2149        _num_heads: usize,
2150        _num_kv_heads: usize,
2151        _head_dim: usize,
2152        _block_size: usize,
2153        _max_num_blocks_per_seq: usize,
2154    ) -> Result<()> {
2155        Err(FerrumError::unsupported(
2156            "paged_varlen_attention_vllm not implemented for this backend",
2157        ))
2158    }
2159
2160    /// Q-tiled vLLM-layout varlen attention. `tile_seqs` and `tile_starts`
2161    /// describe a compact list of q-token tiles, avoiding empty grid blocks
2162    /// for mixed batches that contain both long prefill items and q_len=1
2163    /// decode items. Semantics match [`Self::paged_varlen_attention_vllm`].
2164    #[allow(clippy::too_many_arguments)]
2165    fn paged_varlen_attention_vllm_tiled_q4(
2166        _ctx: &mut Self::Context,
2167        _q: &Self::Buffer,
2168        _k_pool: &Self::Buffer,
2169        _v_pool: &Self::Buffer,
2170        _out: &mut Self::Buffer,
2171        _cu_seqlens_q: &Self::Buffer,
2172        _pos_offsets: &Self::Buffer,
2173        _block_tables: &Self::Buffer,
2174        _tile_seqs: &Self::Buffer,
2175        _tile_starts: &Self::Buffer,
2176        _num_tiles: usize,
2177        _max_kv_len: usize,
2178        _num_heads: usize,
2179        _num_kv_heads: usize,
2180        _head_dim: usize,
2181        _block_size: usize,
2182        _max_num_blocks_per_seq: usize,
2183    ) -> Result<()> {
2184        Err(FerrumError::unsupported(
2185            "paged_varlen_attention_vllm_tiled_q4 not implemented for this backend",
2186        ))
2187    }
2188}
2189
2190// ════════════════════════════════════════════════════════════════════════
2191// Capability bundles — readable type aliases over the supertrait set
2192// ════════════════════════════════════════════════════════════════════════
2193//
2194// Models declare what they need via these bundles instead of spelling out
2195// every supertrait. Rust auto-derives the impl via blanket impls below,
2196// so any backend that satisfies the underlying supertraits automatically
2197// becomes a `LlmBackend` / `QuantLlmBackend` / `MoeLlmBackend`.
2198
2199/// Minimum capability set for a decoder-only LLM: the core compute trait
2200/// plus paged-KV cache + graph-capture support. Every concrete backend
2201/// (CUDA / Metal / CPU) satisfies this.
2202pub trait LlmBackend: Backend + BackendGraph + BackendPagedKv {}
2203impl<T> LlmBackend for T where T: Backend + BackendGraph + BackendPagedKv {}
2204
2205/// LLM backend that also supports quantized weight loading (GPTQ Marlin
2206/// for CUDA; GGUF k-quant for Metal). Required by models that hold
2207/// `Box<dyn Linear<B>>` where the Linear impl might be a quant variant.
2208pub trait QuantLlmBackend: LlmBackend + BackendQuantMarlin + BackendQuantGguf {}
2209impl<T> QuantLlmBackend for T where T: LlmBackend + BackendQuantMarlin + BackendQuantGguf {}
2210
2211/// MoE-capable LLM backend: adds the fused MoE routing + post-op kernels
2212/// to the quant LLM bundle. Required by Qwen3-MoE / future MoE models.
2213pub trait MoeLlmBackend: QuantLlmBackend + BackendMoeFused {}
2214impl<T> MoeLlmBackend for T where T: QuantLlmBackend + BackendMoeFused {}
2215
2216// ════════════════════════════════════════════════════════════════════════
2217// KV cache dtype axis (dim 5 of the 5-dimension architecture)
2218// ════════════════════════════════════════════════════════════════════════
2219//
2220// Each model's KV cache has its own precision independent of the model's
2221// compute precision. vLLM 0.6+ ships INT8 / FP8 KV caches that halve KV
2222// memory at small (<1%) accuracy hit. Today ferrum's KV is hardcoded
2223// FP16 on CUDA / Metal — to support INT8/FP8 KV in a future PR, the
2224// type system needs an explicit axis.
2225//
2226// Phase 4 scope: scaffolding only. All concrete backends impl
2227// `BackendKvDtype<KvFp16>` so existing models keep working unchanged.
2228// Future PR: implement BackendKvDtype<KvInt8> on CUDA + a new model
2229// type-parameter `K: KvDtypeKind` to wire it through.
2230
2231// `KvDtypeKind` + `KvFp16` / `KvBf16` / `KvInt8` / `KvFp8` markers moved
2232// to `ferrum_interfaces::kv_dtype` (no GPU deps, so the right place is
2233// the contract crate). Re-exported here so existing callers keep
2234// compiling against `crate::backend::KvFp16` etc.
2235pub use ferrum_interfaces::kv_dtype::{KvBf16, KvDtypeKind, KvFp16, KvFp8, KvInt8};
2236
2237/// Capability-trait for backends that can store + read a KV cache of
2238/// type `K`.
2239///
2240/// The two associated types carry the K-specific storage shape:
2241///   - `KvBuffer`: per-layer K/V element storage. For `K = KvFp16` it
2242///     is the backend's normal `Self::Buffer` (FP16). For `K = KvInt8`
2243///     it is the backend's INT8 buffer (e.g. `CudaSlice<i8>` on CUDA).
2244///   - `KvScales`: per-token-per-kv-head scales. For `K = KvFp16` this
2245///     is the unit type `()` (no scales). For `K = KvInt8` / `KvFp8`
2246///     it is a backend-specific FP16 buffer.
2247///
2248/// Models that want INT8 KV use:
2249///   `where B: BackendKvDtype<KvInt8>`
2250/// — the buffers in `KvCache<B, KvInt8>` are then `CudaSlice<i8>` and
2251/// `CudaSlice<f16>`, distinct from the FP16 path's `Self::Buffer`.
2252pub trait BackendKvDtype<K: KvDtypeKind>: BackendPagedKv {
2253    /// Per-layer K/V element storage.
2254    type KvBuffer: Send + Sync;
2255    /// Per-token per-kv-head scale storage. `()` for FP16 (no scales).
2256    type KvScales: Send + Sync + Default;
2257}
2258
2259/// INT8 KV cache operations (Dim 5).
2260///
2261/// `BackendKvDtype<KvInt8>` only declares the storage types; it does not
2262/// know how to write INT8 K/V into a paged pool or run paged decode
2263/// attention against an INT8 cache. Those launchers live here so the
2264/// model layer can call them through a single `B: BackendInt8KvOps` bound
2265/// without dropping into backend-specific code.
2266///
2267/// Today only `CudaBackend` provides a real implementation (delegating to
2268/// [`crate::int8_kv::launch_int8_kv_cache_append`] and
2269/// [`crate::int8_kv::launch_int8_paged_decode_attention`]). Other backends
2270/// inherit the default `unimplemented!()` body — the registry factory
2271/// rejects `(Device::CPU/Metal, KvCacheDtype::Int8)` before the model
2272/// gets a chance to call into these.
2273#[allow(clippy::too_many_arguments)]
2274pub trait BackendInt8KvOps: Backend + BackendKvDtype<KvInt8> {
2275    /// Allocate the per-layer INT8 paged cache for one sequence.
2276    /// Default panics — backends without INT8 support never reach this
2277    /// path (factory rejects (Cpu/Metal, Int8) before ensure_kv runs).
2278    fn alloc_paged_int8_layer(
2279        _max_blocks_per_seq: usize,
2280        _block_size: usize,
2281        _num_kv_heads: usize,
2282        _head_dim: usize,
2283    ) -> KvCacheQuant<Self, KvInt8> {
2284        unimplemented!("alloc_paged_int8_layer not supported on this backend")
2285    }
2286
2287    /// Append `tokens` FP16 K/V values into the paged INT8 pool.
2288    /// `paged_block_indices` is the host-side mirror of the per-seq
2289    /// logical→physical block table (already populated at `ensure_kv` time
2290    /// — see `KvCacheQuant::paged_block_indices`). Passing the host slice
2291    /// avoids a per-token D2H + sync barrier; backend computes the slot
2292    /// mapping host-side, async-H2D's it, and chains the append kernel
2293    /// on the same stream — fully overlapping with prior work.
2294    /// `cache_len_before` is the current number of valid tokens; the
2295    /// backend quantizes FP16 → INT8 with per-(token, kv-head) FP16 scale
2296    /// and writes both into the layer's INT8 / scale buffers.
2297    fn int8_kv_append_paged(
2298        _ctx: &mut Self::Context,
2299        _k_in: &Self::Buffer,
2300        _v_in: &Self::Buffer,
2301        _layer_k: &mut <Self as BackendKvDtype<KvInt8>>::KvBuffer,
2302        _layer_v: &mut <Self as BackendKvDtype<KvInt8>>::KvBuffer,
2303        _layer_k_scales: &mut <Self as BackendKvDtype<KvInt8>>::KvScales,
2304        _layer_v_scales: &mut <Self as BackendKvDtype<KvInt8>>::KvScales,
2305        _paged_block_indices: &[u32],
2306        _cache_len_before: usize,
2307        _tokens: usize,
2308        _block_size: usize,
2309        _num_kv_heads: usize,
2310        _head_dim: usize,
2311    ) -> Result<()> {
2312        Err(FerrumError::unsupported(
2313            "int8_kv_append_paged not implemented for this backend",
2314        ))
2315    }
2316
2317    /// Run paged decode attention reading from an INT8 cache. Q is FP16,
2318    /// output is FP16; the kernel dequantizes K/V on the fly using the
2319    /// per-token scales. `valid_kv_len` is the post-append cache length
2320    /// (i.e. the kernel attends over `[0, valid_kv_len)` tokens).
2321    fn int8_paged_decode_attention(
2322        _ctx: &mut Self::Context,
2323        _q: &Self::Buffer,
2324        _layer_k: &<Self as BackendKvDtype<KvInt8>>::KvBuffer,
2325        _layer_v: &<Self as BackendKvDtype<KvInt8>>::KvBuffer,
2326        _layer_k_scales: &<Self as BackendKvDtype<KvInt8>>::KvScales,
2327        _layer_v_scales: &<Self as BackendKvDtype<KvInt8>>::KvScales,
2328        _block_table: &Self::Buffer,
2329        _output: &mut Self::Buffer,
2330        _num_q_heads: usize,
2331        _num_kv_heads: usize,
2332        _head_dim: usize,
2333        _valid_kv_len: usize,
2334        _block_size: usize,
2335        _scale: f32,
2336    ) -> Result<()> {
2337        Err(FerrumError::unsupported(
2338            "int8_paged_decode_attention not implemented for this backend",
2339        ))
2340    }
2341}
2342
2343// Cpu/Metal NOT impl `BackendInt8KvOps` — the trait pivot to
2344// `KvLayer<B>` means `KvInt8: KvLayer<B>` only holds where
2345// `B: BackendInt8KvOps`, so `LlamaFamilyModel<CpuBackend, KvInt8>` is a
2346// compile error (no INT8 KvLayer impl satisfies it). Type system
2347// enforces the constraint without runtime stubs.