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 /// Greedy-decode fast path with sparse repetition penalty.
1571 ///
1572 /// `row_offsets` has length `m + 1` and indexes into `token_ids`; row `r`
1573 /// owns `token_ids[row_offsets[r]..row_offsets[r + 1]]`. Backends should
1574 /// apply each row's `repetition_penalties[r]` to those logits in-place,
1575 /// then run raw or masked argmax and return one token id per row.
1576 #[allow(clippy::too_many_arguments)]
1577 fn argmax_rows_f16_sparse_repetition_penalty(
1578 _ctx: &mut Self::Context,
1579 _logits: &mut Self::Buffer,
1580 _valid_token_mask: Option<(&Self::Buffer, usize)>,
1581 _row_offsets: &Self::Buffer,
1582 _token_ids: &Self::Buffer,
1583 _repetition_penalties: &Self::Buffer,
1584 _total_token_ids: usize,
1585 _m: usize,
1586 _n: usize,
1587 ) -> Result<Vec<u32>> {
1588 Err(FerrumError::unsupported(
1589 "sparse repetition-penalty GPU argmax is not implemented for this backend",
1590 ))
1591 }
1592
1593 /// Load a weight tensor straight from its on-disk byte representation,
1594 /// letting the backend pick its preferred storage dtype.
1595 ///
1596 /// Default impl upcasts bf16/f16 to f32 via an intermediate Vec, matching
1597 /// pre-existing loader behaviour. Backends override this to go straight
1598 /// from raw bytes into a native half-precision buffer (e.g. Metal with
1599 /// `FERRUM_METAL_DTYPE=f16`), avoiding the transient 2× RAM spike.
1600 fn from_weight_bytes(raw: &[u8], src_dtype: SrcDtype) -> Self::Buffer {
1601 let data = src_dtype.to_f32_vec(raw);
1602 Self::from_slice(&data)
1603 }
1604
1605 // (The Phase A3 unified `gemm_quant(QuantWeights, QuantKind)` stub
1606 // that used to live here is superseded by the `load_quant` /
1607 // `gemm_quant(QuantStore)` pair earlier in this trait — same idea,
1608 // but the store hides the per-kind buffer layout so callers don't
1609 // have to construct a per-kind `QuantWeights<'_, Self>` packet.)
1610}
1611
1612// ════════════════════════════════════════════════════════════════════════
1613// BackendPagedKv capability (vLLM-style paged KV cache + paged attention)
1614// ════════════════════════════════════════════════════════════════════════
1615//
1616// Paged KV pool with block-table indirection, plus the paged attention
1617// kernel variants that read through that indirection. CUDA + Metal both
1618// implement the real kernels; CPU `impl BackendPagedKv for CpuBackend {}`
1619// inherits unsupported defaults.
1620
1621/// Capability-trait for backends that support paged KV cache + paged attention.
1622pub trait BackendPagedKv: Backend {
1623 /// Whether this backend has a paged-KV decode path
1624 /// (`paged_decode_attention` etc.). Currently true for Metal, false
1625 /// for CPU. Used to decide the default of `FERRUM_METAL_PAGED_KV` —
1626 /// the `serve` path should opt in automatically when supported so
1627 /// users get the bench-quality concurrent-decode numbers without
1628 /// having to learn the flag.
1629 fn supports_paged_kv() -> bool {
1630 false
1631 }
1632 /// Pre-populate the per-slot device-pointer scratch arrays used by
1633 /// the batched kernels (`kv_cache_append_batched_per_cache` and
1634 /// `flash_attention_batched_per_cache`). Required by the CUDA-graph
1635 /// capture path: the captured graph contains only kernel launches
1636 /// (no captured `memcpy_htod`), so the device scratch must be fresh
1637 /// when the graph replays.
1638 ///
1639 /// Caller passes flat layer-major slices: `k_caches[li * m + i]` and
1640 /// `v_caches[li * m + i]`. Backend extracts each cache's device
1641 /// pointer and writes into its corresponding slot in the device
1642 /// scratch via SYNCHRONOUS memcpy (not captured by stream capture).
1643 ///
1644 /// CUDA-only; other backends fall through to the default
1645 /// `unsupported` and the caller skips the population call.
1646 fn populate_batched_pointers(
1647 _ctx: &mut Self::Context,
1648 _k_caches: &[&Self::Buffer],
1649 _v_caches: &[&Self::Buffer],
1650 _num_layers: usize,
1651 _m: usize,
1652 ) -> Result<()> {
1653 Err(FerrumError::unsupported(
1654 "populate_batched_pointers not implemented for this backend",
1655 ))
1656 }
1657 /// Paged-KV variant of [`Self::split_qkv_norm_rope_into_cache`].
1658 ///
1659 /// Same fused split + qk-norm + RoPE, but K/V are written into a
1660 /// paged pool `[num_blocks, kv_heads, block_size, head_dim]`
1661 /// indexed via `block_table[logical_block]` → physical_block.
1662 /// Q still goes to head-major scratch.
1663 ///
1664 /// Default returns Unsupported. Backends that lack a paged kernel
1665 /// keep using the contiguous variant.
1666 /// `qkv_byte_offset` / `q_out_byte_offset` let the caller pass a
1667 /// slice of a larger batched buffer (used by the multi-seq paged
1668 /// path in `decode_batch_internal`). For single-seq dispatch they
1669 /// should be 0.
1670 #[allow(clippy::too_many_arguments)]
1671 fn split_qkv_norm_rope_into_paged_cache(
1672 _ctx: &mut Self::Context,
1673 _qkv: &Self::Buffer,
1674 _qkv_byte_offset: u64,
1675 _q_norm_w: &Self::Buffer,
1676 _k_norm_w: &Self::Buffer,
1677 _cos: &Self::Buffer,
1678 _sin: &Self::Buffer,
1679 _q_out: &mut Self::Buffer,
1680 _q_out_byte_offset: u64,
1681 _cache_k: &mut Self::Buffer,
1682 _cache_v: &mut Self::Buffer,
1683 _block_table: &Self::Buffer,
1684 _tokens: usize,
1685 _q_heads: usize,
1686 _kv_heads: usize,
1687 _head_dim: usize,
1688 _pos_offset: usize,
1689 _eps: f32,
1690 _qk_mode: i32,
1691 _cache_len: usize,
1692 _block_size: usize,
1693 _max_num_blocks_per_seq: usize,
1694 ) -> Result<()> {
1695 Err(FerrumError::unsupported(
1696 "split_qkv_norm_rope_into_paged_cache not implemented for this backend",
1697 ))
1698 }
1699 /// Paged-KV variant of [`Self::flash_attention`].
1700 ///
1701 /// Decode (`q_len == 1`):
1702 /// `q`/`out`: `[num_seqs, num_heads, head_dim]` (token-major)
1703 ///
1704 /// Causal prefill (`q_len > 1`, single seq):
1705 /// `q`/`out`: `[num_heads, q_len, head_dim]` (head-major — the
1706 /// layout produced by `split_qkv_norm_rope_into_paged_cache`)
1707 /// The kernel applies a per-q-token causal mask using
1708 /// `context_lens[seq]` as the FINAL kv_len (= `pos_offset + q_len`):
1709 /// token i sees positions `[0, context_lens - q_len + 1 + i)`.
1710 ///
1711 /// Common to both:
1712 /// `k_pool`/`v_pool`: `[num_blocks, num_kv_heads, block_size, head_dim]`
1713 /// `block_tables`: `[num_seqs, max_num_blocks_per_seq]` u32
1714 /// `context_lens`: `[num_seqs]` u32
1715 ///
1716 /// Backends without a paged kernel return Unsupported; callers are
1717 /// expected to fall back to contiguous KV.
1718 #[allow(clippy::too_many_arguments)]
1719 fn paged_decode_attention(
1720 _ctx: &mut Self::Context,
1721 _q: &Self::Buffer,
1722 _k_pool: &Self::Buffer,
1723 _v_pool: &Self::Buffer,
1724 _out: &mut Self::Buffer,
1725 _block_tables: &Self::Buffer,
1726 _context_lens: &Self::Buffer,
1727 _num_seqs: usize,
1728 _num_heads: usize,
1729 _num_kv_heads: usize,
1730 _head_dim: usize,
1731 _block_size: usize,
1732 _max_num_blocks_per_seq: usize,
1733 _q_len: usize,
1734 ) -> Result<()> {
1735 Err(FerrumError::unsupported(
1736 "paged_decode_attention not implemented for this backend",
1737 ))
1738 }
1739 /// Capability: does this backend implement
1740 /// `split_qkv_norm_rope_into_paged_cache_varlen` and
1741 /// `paged_varlen_attention`? Required by the unified mixed-batch
1742 /// forward path used by `LlamaFamilyModel::unified_forward`. Default
1743 /// false; backends that ship the varlen kernels override.
1744 fn supports_varlen_qkv() -> bool {
1745 false
1746 }
1747 /// Varlen variant of [`Self::split_qkv_norm_rope_into_paged_cache`].
1748 ///
1749 /// Single launch covering ALL sequences in the batch. Reads
1750 /// `pos_offsets[seq]`, `cu_seqlens_q[seq]`, and the per-seq
1751 /// block_table from device buffers — graph-capturable (the per-iter
1752 /// state is in buffers, not kernel scalars). Replaces the per-item
1753 /// dispatch loop in `unified_forward_layer` with one call.
1754 ///
1755 /// Layouts:
1756 /// - `qkv`: `[m_total, q_dim + 2 * kv_dim]` token-major
1757 /// - `q_out`: `[m_total, q_heads, head_dim]` token-major (matches
1758 /// what `paged_varlen_attention` reads)
1759 /// - `cache_k` / `cache_v`: paged pool same as `paged_varlen_attention`
1760 /// - `cu_seqlens_q`: `[num_seqs + 1]` u32 prefix sum
1761 /// - `pos_offsets`: `[num_seqs]` u32, starting kv_pos per seq
1762 /// - `block_tables`: `[num_seqs, max_blocks_per_seq]` i32 stacked
1763 #[allow(clippy::too_many_arguments)]
1764 fn split_qkv_norm_rope_into_paged_cache_varlen(
1765 _ctx: &mut Self::Context,
1766 _qkv: &Self::Buffer,
1767 _q_norm_w: &Self::Buffer,
1768 _k_norm_w: &Self::Buffer,
1769 _cos: &Self::Buffer,
1770 _sin: &Self::Buffer,
1771 _q_out: &mut Self::Buffer,
1772 _cache_k: &mut Self::Buffer,
1773 _cache_v: &mut Self::Buffer,
1774 _cu_seqlens_q: &Self::Buffer,
1775 _pos_offsets: &Self::Buffer,
1776 _block_tables: &Self::Buffer,
1777 _num_seqs: usize,
1778 _m_total: usize,
1779 _q_heads: usize,
1780 _kv_heads: usize,
1781 _head_dim: usize,
1782 _eps: f32,
1783 _qk_mode: i32,
1784 _block_size: usize,
1785 _max_blocks_per_seq: usize,
1786 ) -> Result<()> {
1787 Err(FerrumError::unsupported(
1788 "split_qkv_norm_rope_into_paged_cache_varlen not implemented for this backend",
1789 ))
1790 }
1791 /// Variable-length paged attention with GQA + causal mask.
1792 ///
1793 /// Supports a unified mixed batch where each sequence contributes
1794 /// 1 (decode) or N (prefill chunk) query tokens — the workhorse for
1795 /// chunked-prefill. See `kernels/paged_varlen_attention.cu` for the
1796 /// kernel itself.
1797 ///
1798 /// Layouts:
1799 /// - `q` / `out`: `[total_q_tokens, num_heads, head_dim]` (token-
1800 /// major, FP16). `total_q_tokens` = `cu_seqlens_q[num_seqs]`.
1801 /// - `k_pool` / `v_pool`: paged block pool, layout matches
1802 /// `paged_decode_attention`.
1803 /// - `cu_seqlens_q`: `[num_seqs + 1]` u32 prefix sum, with
1804 /// `cu_seqlens_q[0] = 0` and `cu_seqlens_q[num_seqs] = total_q_tokens`.
1805 /// - `pos_offsets`: `[num_seqs]` u32, the starting absolute KV
1806 /// position of each seq's first q token (= prior `kv_len`).
1807 /// - `block_tables`: `[num_seqs, max_num_blocks_per_seq]` i32 grid.
1808 ///
1809 /// Each query token attends causally to KV positions
1810 /// `[0, pos_offsets[s] + local_idx]` when `sliding_window == 0`, or
1811 /// only the most recent `sliding_window` positions when non-zero.
1812 #[allow(clippy::too_many_arguments)]
1813 fn paged_varlen_attention(
1814 _ctx: &mut Self::Context,
1815 _q: &Self::Buffer,
1816 _k_pool: &Self::Buffer,
1817 _v_pool: &Self::Buffer,
1818 _out: &mut Self::Buffer,
1819 _cu_seqlens_q: &Self::Buffer,
1820 _pos_offsets: &Self::Buffer,
1821 _block_tables: &Self::Buffer,
1822 _num_seqs: usize,
1823 _total_q_tokens: usize,
1824 _max_kv_len: usize,
1825 _num_heads: usize,
1826 _num_kv_heads: usize,
1827 _head_dim: usize,
1828 _sliding_window: usize,
1829 _block_size: usize,
1830 _max_num_blocks_per_seq: usize,
1831 ) -> Result<()> {
1832 Err(FerrumError::unsupported(
1833 "paged_varlen_attention not implemented for this backend",
1834 ))
1835 }
1836
1837 /// Opt-in vLLM FlashAttention-2 FFI path for FA-layout paged KV.
1838 ///
1839 /// This is intentionally separate from [`Self::paged_varlen_attention`]:
1840 /// it needs the final per-sequence KV lengths (`seq_lens`) and an explicit
1841 /// LSE scratch buffer because the external FA2 runner writes softmax LSE.
1842 /// Default returns Err(unsupported); CUDA overrides when a runtime shim is
1843 /// provided via `FERRUM_FA2_DIRECT_FFI_SHIM`.
1844 #[allow(clippy::too_many_arguments)]
1845 fn paged_varlen_attention_fa2_ffi(
1846 _ctx: &mut Self::Context,
1847 _q: &Self::Buffer,
1848 _k_pool: &Self::Buffer,
1849 _v_pool: &Self::Buffer,
1850 _out: &mut Self::Buffer,
1851 _lse: &mut Self::Buffer,
1852 _cu_seqlens_q: &Self::Buffer,
1853 _seq_lens: &Self::Buffer,
1854 _block_tables: &Self::Buffer,
1855 _num_seqs: usize,
1856 _total_q_tokens: usize,
1857 _max_q_len: usize,
1858 _max_kv_len: usize,
1859 _num_heads: usize,
1860 _num_kv_heads: usize,
1861 _head_dim: usize,
1862 _block_size: usize,
1863 _max_num_blocks_per_seq: usize,
1864 ) -> Result<()> {
1865 Err(FerrumError::unsupported(
1866 "paged_varlen_attention_fa2_ffi not implemented for this backend",
1867 ))
1868 }
1869
1870 /// Batched paged decode attention — multi-seq, single token per seq.
1871 /// Faster path for the unified_forward layer when m_total == num_seqs
1872 /// (every item is a single-token decode). Skips the cu_seqlens_q
1873 /// linear scan that `paged_varlen_attention` does in the fully-mixed
1874 /// case.
1875 ///
1876 /// Layouts:
1877 /// q : [num_seqs, num_q_heads, head_dim]
1878 /// k_pool/v_pool : paged pool (same as paged_varlen)
1879 /// block_tables : [num_seqs, max_num_blocks_per_seq]
1880 /// valid_kv_lens : [num_seqs] — current kv_len per seq
1881 /// out : [num_seqs, num_q_heads, head_dim]
1882 ///
1883 /// Default returns Err(unsupported); CUDA backend overrides.
1884 #[allow(clippy::too_many_arguments)]
1885 fn paged_batched_decode_attention(
1886 _ctx: &mut Self::Context,
1887 _q: &Self::Buffer,
1888 _k_pool: &Self::Buffer,
1889 _v_pool: &Self::Buffer,
1890 _out: &mut Self::Buffer,
1891 _block_tables: &Self::Buffer,
1892 _valid_kv_lens: &Self::Buffer,
1893 _num_seqs: usize,
1894 _max_kv_len: usize,
1895 _num_heads: usize,
1896 _num_kv_heads: usize,
1897 _head_dim: usize,
1898 _block_size: usize,
1899 _max_num_blocks_per_seq: usize,
1900 ) -> Result<()> {
1901 Err(FerrumError::unsupported(
1902 "paged_batched_decode_attention not implemented for this backend",
1903 ))
1904 }
1905
1906 /// Capability: backend has vLLM-layout paged KV write kernels and the
1907 /// `paged_attention_v2` decode kernel. Models that opt into this layout
1908 /// at construction time (via `FERRUM_USE_VLLM_PAGED_ATTN=1`) must
1909 /// dispatch ALL paged writes and reads through the `_vllm` variants —
1910 /// the layouts are not compatible. Default `false`.
1911 fn supports_vllm_paged_attn() -> bool {
1912 false
1913 }
1914
1915 /// Qwen3.5 full-attention uses separate q/k/v projections and partial
1916 /// RoPE/gated-Q layout, so it cannot use the fused-QKV paged writer
1917 /// directly. Backends that implement this method can write those
1918 /// separate projections into Ferrum's legacy paged pool consumed by
1919 /// [`Self::paged_batched_decode_attention`].
1920 fn supports_qwen35_paged_qkv() -> bool {
1921 false
1922 }
1923
1924 #[allow(clippy::too_many_arguments)]
1925 fn qwen35_split_qkv_norm_rope_into_paged_cache_varlen(
1926 _ctx: &mut Self::Context,
1927 _query_raw: &Self::Buffer,
1928 _key_raw: &Self::Buffer,
1929 _value_raw: &Self::Buffer,
1930 _q_norm_w: &Self::Buffer,
1931 _k_norm_w: &Self::Buffer,
1932 _cos: &Self::Buffer,
1933 _sin: &Self::Buffer,
1934 _q_out: &mut Self::Buffer,
1935 _cache_k: &mut Self::Buffer,
1936 _cache_v: &mut Self::Buffer,
1937 _cu_seqlens_q: &Self::Buffer,
1938 _token_seq_indices: &Self::Buffer,
1939 _pos_offsets: &Self::Buffer,
1940 _block_tables: &Self::Buffer,
1941 _num_seqs: usize,
1942 _total_q_tokens: usize,
1943 _q_heads: usize,
1944 _kv_heads: usize,
1945 _head_dim: usize,
1946 _rope_dim: usize,
1947 _q_proj_stride: usize,
1948 _q_head_stride: usize,
1949 _kv_proj_stride: usize,
1950 _eps: f32,
1951 _qk_mode: i32,
1952 _block_size: usize,
1953 _max_blocks_per_seq: usize,
1954 ) -> Result<()> {
1955 Err(FerrumError::unsupported(
1956 "qwen35_split_qkv_norm_rope_into_paged_cache_varlen not implemented for this backend",
1957 ))
1958 }
1959
1960 /// vLLM-layout variant of the Qwen3.5 separate q/k/v writer. K/V are
1961 /// written in the layout consumed by [`Self::paged_decode_attention_v2`],
1962 /// while Q remains token-major `[total_q_tokens, q_heads, head_dim]`.
1963 fn supports_qwen35_paged_qkv_vllm() -> bool {
1964 false
1965 }
1966
1967 #[allow(clippy::too_many_arguments)]
1968 fn qwen35_split_qkv_norm_rope_into_paged_cache_varlen_vllm(
1969 _ctx: &mut Self::Context,
1970 _query_raw: &Self::Buffer,
1971 _key_raw: &Self::Buffer,
1972 _value_raw: &Self::Buffer,
1973 _q_norm_w: &Self::Buffer,
1974 _k_norm_w: &Self::Buffer,
1975 _cos: &Self::Buffer,
1976 _sin: &Self::Buffer,
1977 _q_out: &mut Self::Buffer,
1978 _cache_k: &mut Self::Buffer,
1979 _cache_v: &mut Self::Buffer,
1980 _cu_seqlens_q: &Self::Buffer,
1981 _token_seq_indices: &Self::Buffer,
1982 _pos_offsets: &Self::Buffer,
1983 _block_tables: &Self::Buffer,
1984 _num_seqs: usize,
1985 _total_q_tokens: usize,
1986 _q_heads: usize,
1987 _kv_heads: usize,
1988 _head_dim: usize,
1989 _rope_dim: usize,
1990 _q_proj_stride: usize,
1991 _q_head_stride: usize,
1992 _kv_proj_stride: usize,
1993 _eps: f32,
1994 _qk_mode: i32,
1995 _block_size: usize,
1996 _max_blocks_per_seq: usize,
1997 ) -> Result<()> {
1998 Err(FerrumError::unsupported(
1999 "qwen35_split_qkv_norm_rope_into_paged_cache_varlen_vllm not implemented for this backend",
2000 ))
2001 }
2002
2003 /// vLLM-layout variant of
2004 /// [`Self::split_qkv_norm_rope_into_paged_cache`]. K/V are written in
2005 /// vLLM's `paged_attention_v2` layout: K is
2006 /// `[num_blocks, kv_heads, head_dim/x, block_size, x]` (x = 16/sizeof(elem)),
2007 /// V is `[num_blocks, kv_heads, head_dim, block_size]`. Q output and
2008 /// every other argument matches the non-vllm variant exactly so the
2009 /// model layer can swap dispatchers based on a single flag.
2010 #[allow(clippy::too_many_arguments)]
2011 fn split_qkv_norm_rope_into_paged_cache_vllm(
2012 _ctx: &mut Self::Context,
2013 _qkv: &Self::Buffer,
2014 _qkv_byte_offset: u64,
2015 _q_norm_w: &Self::Buffer,
2016 _k_norm_w: &Self::Buffer,
2017 _cos: &Self::Buffer,
2018 _sin: &Self::Buffer,
2019 _q_out: &mut Self::Buffer,
2020 _q_out_byte_offset: u64,
2021 _cache_k: &mut Self::Buffer,
2022 _cache_v: &mut Self::Buffer,
2023 _block_table: &Self::Buffer,
2024 _tokens: usize,
2025 _q_heads: usize,
2026 _kv_heads: usize,
2027 _head_dim: usize,
2028 _pos_offset: usize,
2029 _eps: f32,
2030 _qk_mode: i32,
2031 _cache_len: usize,
2032 _block_size: usize,
2033 _max_num_blocks_per_seq: usize,
2034 ) -> Result<()> {
2035 Err(FerrumError::unsupported(
2036 "split_qkv_norm_rope_into_paged_cache_vllm not implemented for this backend",
2037 ))
2038 }
2039
2040 /// vLLM-layout variant of
2041 /// [`Self::split_qkv_norm_rope_into_paged_cache_varlen`]. Same signature
2042 /// — only the K/V cache layout changes.
2043 #[allow(clippy::too_many_arguments)]
2044 fn split_qkv_norm_rope_into_paged_cache_varlen_vllm(
2045 _ctx: &mut Self::Context,
2046 _qkv: &Self::Buffer,
2047 _q_norm_w: &Self::Buffer,
2048 _k_norm_w: &Self::Buffer,
2049 _cos: &Self::Buffer,
2050 _sin: &Self::Buffer,
2051 _q_out: &mut Self::Buffer,
2052 _cache_k: &mut Self::Buffer,
2053 _cache_v: &mut Self::Buffer,
2054 _cu_seqlens_q: &Self::Buffer,
2055 _pos_offsets: &Self::Buffer,
2056 _block_tables: &Self::Buffer,
2057 _num_seqs: usize,
2058 _m_total: usize,
2059 _q_heads: usize,
2060 _kv_heads: usize,
2061 _head_dim: usize,
2062 _eps: f32,
2063 _qk_mode: i32,
2064 _block_size: usize,
2065 _max_blocks_per_seq: usize,
2066 ) -> Result<()> {
2067 Err(FerrumError::unsupported(
2068 "split_qkv_norm_rope_into_paged_cache_varlen_vllm not implemented for this backend",
2069 ))
2070 }
2071
2072 /// vLLM `paged_attention_v2` — multi-partition split-K decode attention
2073 /// reading the vLLM K/V layout. `q_len` is implicitly 1 (decode only;
2074 /// vLLM's v2 kernel does not support q_len > 1). `max_seq_len` is the
2075 /// max kv_len across the batch — used to size the partition reduction.
2076 #[allow(clippy::too_many_arguments)]
2077 fn paged_decode_attention_v2(
2078 _ctx: &mut Self::Context,
2079 _q: &Self::Buffer,
2080 _k_pool: &Self::Buffer,
2081 _v_pool: &Self::Buffer,
2082 _out: &mut Self::Buffer,
2083 _block_tables: &Self::Buffer,
2084 _context_lens: &Self::Buffer,
2085 _num_seqs: usize,
2086 _num_heads: usize,
2087 _num_kv_heads: usize,
2088 _head_dim: usize,
2089 _block_size: usize,
2090 _max_num_blocks_per_seq: usize,
2091 _max_seq_len: usize,
2092 ) -> Result<()> {
2093 Err(FerrumError::unsupported(
2094 "paged_decode_attention_v2 not implemented for this backend",
2095 ))
2096 }
2097
2098 /// q_len>1 prefill/chunk-prefill attention over vLLM-layout paged KV.
2099 /// This keeps cache layout consistent when `FERRUM_USE_VLLM_PAGED_ATTN=1`
2100 /// and the prompt path writes K/V in the layout consumed later by
2101 /// `paged_decode_attention_v2`.
2102 #[allow(clippy::too_many_arguments)]
2103 fn paged_varlen_attention_vllm_layout(
2104 _ctx: &mut Self::Context,
2105 _q: &Self::Buffer,
2106 _k_pool: &Self::Buffer,
2107 _v_pool: &Self::Buffer,
2108 _out: &mut Self::Buffer,
2109 _block_tables: &Self::Buffer,
2110 _context_lens: &Self::Buffer,
2111 _num_seqs: usize,
2112 _num_heads: usize,
2113 _num_kv_heads: usize,
2114 _head_dim: usize,
2115 _block_size: usize,
2116 _max_num_blocks_per_seq: usize,
2117 _q_len: usize,
2118 ) -> Result<()> {
2119 Err(FerrumError::unsupported(
2120 "paged_varlen_attention_vllm_layout not implemented for this backend",
2121 ))
2122 }
2123
2124 /// Variable-length paged attention over vLLM-layout paged KV.
2125 ///
2126 /// Unlike [`Self::paged_varlen_attention_vllm_layout`], this accepts the
2127 /// same varlen index tensors as [`Self::paged_varlen_attention`] and writes
2128 /// token-major output directly. It is the unified mixed-batch companion for
2129 /// `split_qkv_norm_rope_into_paged_cache_varlen_vllm`.
2130 #[allow(clippy::too_many_arguments)]
2131 fn paged_varlen_attention_vllm(
2132 _ctx: &mut Self::Context,
2133 _q: &Self::Buffer,
2134 _k_pool: &Self::Buffer,
2135 _v_pool: &Self::Buffer,
2136 _out: &mut Self::Buffer,
2137 _cu_seqlens_q: &Self::Buffer,
2138 _pos_offsets: &Self::Buffer,
2139 _block_tables: &Self::Buffer,
2140 _num_seqs: usize,
2141 _total_q_tokens: usize,
2142 _max_kv_len: usize,
2143 _num_heads: usize,
2144 _num_kv_heads: usize,
2145 _head_dim: usize,
2146 _block_size: usize,
2147 _max_num_blocks_per_seq: usize,
2148 ) -> Result<()> {
2149 Err(FerrumError::unsupported(
2150 "paged_varlen_attention_vllm not implemented for this backend",
2151 ))
2152 }
2153
2154 /// Q-tiled vLLM-layout varlen attention. `tile_seqs` and `tile_starts`
2155 /// describe a compact list of q-token tiles, avoiding empty grid blocks
2156 /// for mixed batches that contain both long prefill items and q_len=1
2157 /// decode items. Semantics match [`Self::paged_varlen_attention_vllm`].
2158 #[allow(clippy::too_many_arguments)]
2159 fn paged_varlen_attention_vllm_tiled_q4(
2160 _ctx: &mut Self::Context,
2161 _q: &Self::Buffer,
2162 _k_pool: &Self::Buffer,
2163 _v_pool: &Self::Buffer,
2164 _out: &mut Self::Buffer,
2165 _cu_seqlens_q: &Self::Buffer,
2166 _pos_offsets: &Self::Buffer,
2167 _block_tables: &Self::Buffer,
2168 _tile_seqs: &Self::Buffer,
2169 _tile_starts: &Self::Buffer,
2170 _num_tiles: usize,
2171 _max_kv_len: usize,
2172 _num_heads: usize,
2173 _num_kv_heads: usize,
2174 _head_dim: usize,
2175 _block_size: usize,
2176 _max_num_blocks_per_seq: usize,
2177 ) -> Result<()> {
2178 Err(FerrumError::unsupported(
2179 "paged_varlen_attention_vllm_tiled_q4 not implemented for this backend",
2180 ))
2181 }
2182}
2183
2184// ════════════════════════════════════════════════════════════════════════
2185// Capability bundles — readable type aliases over the supertrait set
2186// ════════════════════════════════════════════════════════════════════════
2187//
2188// Models declare what they need via these bundles instead of spelling out
2189// every supertrait. Rust auto-derives the impl via blanket impls below,
2190// so any backend that satisfies the underlying supertraits automatically
2191// becomes a `LlmBackend` / `QuantLlmBackend` / `MoeLlmBackend`.
2192
2193/// Minimum capability set for a decoder-only LLM: the core compute trait
2194/// plus paged-KV cache + graph-capture support. Every concrete backend
2195/// (CUDA / Metal / CPU) satisfies this.
2196pub trait LlmBackend: Backend + BackendGraph + BackendPagedKv {}
2197impl<T> LlmBackend for T where T: Backend + BackendGraph + BackendPagedKv {}
2198
2199/// LLM backend that also supports quantized weight loading (GPTQ Marlin
2200/// for CUDA; GGUF k-quant for Metal). Required by models that hold
2201/// `Box<dyn Linear<B>>` where the Linear impl might be a quant variant.
2202pub trait QuantLlmBackend: LlmBackend + BackendQuantMarlin + BackendQuantGguf {}
2203impl<T> QuantLlmBackend for T where T: LlmBackend + BackendQuantMarlin + BackendQuantGguf {}
2204
2205/// MoE-capable LLM backend: adds the fused MoE routing + post-op kernels
2206/// to the quant LLM bundle. Required by Qwen3-MoE / future MoE models.
2207pub trait MoeLlmBackend: QuantLlmBackend + BackendMoeFused {}
2208impl<T> MoeLlmBackend for T where T: QuantLlmBackend + BackendMoeFused {}
2209
2210// ════════════════════════════════════════════════════════════════════════
2211// KV cache dtype axis (dim 5 of the 5-dimension architecture)
2212// ════════════════════════════════════════════════════════════════════════
2213//
2214// Each model's KV cache has its own precision independent of the model's
2215// compute precision. vLLM 0.6+ ships INT8 / FP8 KV caches that halve KV
2216// memory at small (<1%) accuracy hit. Today ferrum's KV is hardcoded
2217// FP16 on CUDA / Metal — to support INT8/FP8 KV in a future PR, the
2218// type system needs an explicit axis.
2219//
2220// Phase 4 scope: scaffolding only. All concrete backends impl
2221// `BackendKvDtype<KvFp16>` so existing models keep working unchanged.
2222// Future PR: implement BackendKvDtype<KvInt8> on CUDA + a new model
2223// type-parameter `K: KvDtypeKind` to wire it through.
2224
2225// `KvDtypeKind` + `KvFp16` / `KvBf16` / `KvInt8` / `KvFp8` markers moved
2226// to `ferrum_interfaces::kv_dtype` (no GPU deps, so the right place is
2227// the contract crate). Re-exported here so existing callers keep
2228// compiling against `crate::backend::KvFp16` etc.
2229pub use ferrum_interfaces::kv_dtype::{KvBf16, KvDtypeKind, KvFp16, KvFp8, KvInt8};
2230
2231/// Capability-trait for backends that can store + read a KV cache of
2232/// type `K`.
2233///
2234/// The two associated types carry the K-specific storage shape:
2235/// - `KvBuffer`: per-layer K/V element storage. For `K = KvFp16` it
2236/// is the backend's normal `Self::Buffer` (FP16). For `K = KvInt8`
2237/// it is the backend's INT8 buffer (e.g. `CudaSlice<i8>` on CUDA).
2238/// - `KvScales`: per-token-per-kv-head scales. For `K = KvFp16` this
2239/// is the unit type `()` (no scales). For `K = KvInt8` / `KvFp8`
2240/// it is a backend-specific FP16 buffer.
2241///
2242/// Models that want INT8 KV use:
2243/// `where B: BackendKvDtype<KvInt8>`
2244/// — the buffers in `KvCache<B, KvInt8>` are then `CudaSlice<i8>` and
2245/// `CudaSlice<f16>`, distinct from the FP16 path's `Self::Buffer`.
2246pub trait BackendKvDtype<K: KvDtypeKind>: BackendPagedKv {
2247 /// Per-layer K/V element storage.
2248 type KvBuffer: Send + Sync;
2249 /// Per-token per-kv-head scale storage. `()` for FP16 (no scales).
2250 type KvScales: Send + Sync + Default;
2251}
2252
2253/// INT8 KV cache operations (Dim 5).
2254///
2255/// `BackendKvDtype<KvInt8>` only declares the storage types; it does not
2256/// know how to write INT8 K/V into a paged pool or run paged decode
2257/// attention against an INT8 cache. Those launchers live here so the
2258/// model layer can call them through a single `B: BackendInt8KvOps` bound
2259/// without dropping into backend-specific code.
2260///
2261/// Today only `CudaBackend` provides a real implementation (delegating to
2262/// [`crate::int8_kv::launch_int8_kv_cache_append`] and
2263/// [`crate::int8_kv::launch_int8_paged_decode_attention`]). Other backends
2264/// inherit the default `unimplemented!()` body — the registry factory
2265/// rejects `(Device::CPU/Metal, KvCacheDtype::Int8)` before the model
2266/// gets a chance to call into these.
2267#[allow(clippy::too_many_arguments)]
2268pub trait BackendInt8KvOps: Backend + BackendKvDtype<KvInt8> {
2269 /// Allocate the per-layer INT8 paged cache for one sequence.
2270 /// Default panics — backends without INT8 support never reach this
2271 /// path (factory rejects (Cpu/Metal, Int8) before ensure_kv runs).
2272 fn alloc_paged_int8_layer(
2273 _max_blocks_per_seq: usize,
2274 _block_size: usize,
2275 _num_kv_heads: usize,
2276 _head_dim: usize,
2277 ) -> KvCacheQuant<Self, KvInt8> {
2278 unimplemented!("alloc_paged_int8_layer not supported on this backend")
2279 }
2280
2281 /// Append `tokens` FP16 K/V values into the paged INT8 pool.
2282 /// `paged_block_indices` is the host-side mirror of the per-seq
2283 /// logical→physical block table (already populated at `ensure_kv` time
2284 /// — see `KvCacheQuant::paged_block_indices`). Passing the host slice
2285 /// avoids a per-token D2H + sync barrier; backend computes the slot
2286 /// mapping host-side, async-H2D's it, and chains the append kernel
2287 /// on the same stream — fully overlapping with prior work.
2288 /// `cache_len_before` is the current number of valid tokens; the
2289 /// backend quantizes FP16 → INT8 with per-(token, kv-head) FP16 scale
2290 /// and writes both into the layer's INT8 / scale buffers.
2291 fn int8_kv_append_paged(
2292 _ctx: &mut Self::Context,
2293 _k_in: &Self::Buffer,
2294 _v_in: &Self::Buffer,
2295 _layer_k: &mut <Self as BackendKvDtype<KvInt8>>::KvBuffer,
2296 _layer_v: &mut <Self as BackendKvDtype<KvInt8>>::KvBuffer,
2297 _layer_k_scales: &mut <Self as BackendKvDtype<KvInt8>>::KvScales,
2298 _layer_v_scales: &mut <Self as BackendKvDtype<KvInt8>>::KvScales,
2299 _paged_block_indices: &[u32],
2300 _cache_len_before: usize,
2301 _tokens: usize,
2302 _block_size: usize,
2303 _num_kv_heads: usize,
2304 _head_dim: usize,
2305 ) -> Result<()> {
2306 Err(FerrumError::unsupported(
2307 "int8_kv_append_paged not implemented for this backend",
2308 ))
2309 }
2310
2311 /// Run paged decode attention reading from an INT8 cache. Q is FP16,
2312 /// output is FP16; the kernel dequantizes K/V on the fly using the
2313 /// per-token scales. `valid_kv_len` is the post-append cache length
2314 /// (i.e. the kernel attends over `[0, valid_kv_len)` tokens).
2315 fn int8_paged_decode_attention(
2316 _ctx: &mut Self::Context,
2317 _q: &Self::Buffer,
2318 _layer_k: &<Self as BackendKvDtype<KvInt8>>::KvBuffer,
2319 _layer_v: &<Self as BackendKvDtype<KvInt8>>::KvBuffer,
2320 _layer_k_scales: &<Self as BackendKvDtype<KvInt8>>::KvScales,
2321 _layer_v_scales: &<Self as BackendKvDtype<KvInt8>>::KvScales,
2322 _block_table: &Self::Buffer,
2323 _output: &mut Self::Buffer,
2324 _num_q_heads: usize,
2325 _num_kv_heads: usize,
2326 _head_dim: usize,
2327 _valid_kv_len: usize,
2328 _block_size: usize,
2329 _scale: f32,
2330 ) -> Result<()> {
2331 Err(FerrumError::unsupported(
2332 "int8_paged_decode_attention not implemented for this backend",
2333 ))
2334 }
2335}
2336
2337// Cpu/Metal NOT impl `BackendInt8KvOps` — the trait pivot to
2338// `KvLayer<B>` means `KvInt8: KvLayer<B>` only holds where
2339// `B: BackendInt8KvOps`, so `LlamaFamilyModel<CpuBackend, KvInt8>` is a
2340// compile error (no INT8 KvLayer impl satisfies it). Type system
2341// enforces the constraint without runtime stubs.