metrale_model_layers/lora/moe_row_adapter.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: GPU-free MoE LoRA routing: the per-request fold decision
4//! ([`resolve_moe_lora_route`]), the refusal of a decode batch the
5//! single-active fold cannot serve, and the per-row adapter maps the MoE
6//! LoRA kernels read (`< 0` means no fold on that row).
7//!
8//! Owner: model-layers (lora).
9//! Invariants: none beyond the types.
10
11use crate::layer::MoeLoraRoute;
12
13/// 2026-09-25: An error when `route` is `Refuse`. The batched decode entries
14/// (model-engine `decode_batch_compute_main`, `mixed_forward`) call it before
15/// any graph lookup: [`build_moe_row_adapter_decode`] writes a `Refuse` row as
16/// `-1`, so the batch would otherwise run that request without its adapter.
17pub fn ensure_decode_route_servable(route: MoeLoraRoute, path: &str) -> anyhow::Result<()> {
18 anyhow::ensure!(
19 !matches!(route, MoeLoraRoute::Refuse),
20 "MoE LoRA {path}: a sequence routes to a non-active adapter under single-active \
21 phase-1; refusing rather than mis-folding the active adapter. One adapter per batch."
22 );
23 Ok(())
24}
25
26/// 2026-09-25: The MoE LoRA fold decision for one request:
27/// - `!has_moe_lora` ⇒ `Fold`;
28/// - `adapter_slot < 0` (base request) ⇒ `Skip`;
29/// - `adapter_slot == active` ⇒ `Fold`;
30/// - any other slot ⇒ `Refuse`, since only the active adapter is installed.
31///
32/// Model-engine's `moe_lora_route` passes `has_moe_lora` = "a LoRA pool is
33/// loaded" and `active = -1` when none is.
34pub fn resolve_moe_lora_route(adapter_slot: i32, active: i32, has_moe_lora: bool) -> MoeLoraRoute {
35 if !has_moe_lora {
36 return MoeLoraRoute::Fold;
37 }
38 if adapter_slot < 0 {
39 return MoeLoraRoute::Skip;
40 }
41 if adapter_slot == active {
42 MoeLoraRoute::Fold
43 } else {
44 MoeLoraRoute::Refuse
45 }
46}
47
48/// 2026-09-25: The `[total_tokens]` per-row adapter map of a packed prefill
49/// batch. `cu_seqlens_host` is the `[batch + 1]` prefix sum of per-stream token
50/// counts; stream `b`'s `adapter_slots[b]` is written, unchanged, over rows
51/// `cu_seqlens_host[b]..cu_seqlens_host[b + 1]`. Only tests call it today.
52///
53/// `None`, before any row is written, when `cu_seqlens_host` has fewer than two
54/// entries, `adapter_slots` is not `batch` long, the first entry is not 0, or
55/// any boundary is negative or decreasing.
56pub fn build_moe_row_adapter_host(
57 cu_seqlens_host: &[i32],
58 adapter_slots: &[i32],
59) -> Option<Vec<i32>> {
60 if cu_seqlens_host.len() < 2 {
61 return None;
62 }
63 let batch = cu_seqlens_host.len() - 1;
64 if adapter_slots.len() != batch {
65 return None;
66 }
67 if cu_seqlens_host[0] != 0 {
68 return None;
69 }
70 // 2026-09-25: Check every boundary before sizing the map from the last
71 // one, so `[0, 4, 2]` cannot write past it.
72 for b in 0..batch {
73 let start = cu_seqlens_host[b];
74 let end = cu_seqlens_host[b + 1];
75 if start < 0 || end < start {
76 return None;
77 }
78 }
79 let total = cu_seqlens_host[batch];
80 let mut map = vec![-1i32; total as usize];
81 for b in 0..batch {
82 let start = cu_seqlens_host[b];
83 let end = cu_seqlens_host[b + 1];
84 let slot = adapter_slots[b];
85 for row in start..end {
86 map[row as usize] = slot;
87 }
88 }
89 Some(map)
90}
91
92/// 2026-09-25: The `[padded_n]` per-row adapter map of a decode batch, one
93/// token per sequence, which `moe_lora_gather_bgmv.cu` reads as
94/// `row_adapter[row / top_k]` and skips when negative. Row `i` is `active`
95/// when `resolve_moe_lora_route` gives `Fold`, else `-1`; so `Skip`, `Refuse`
96/// and padding rows (`i >= adapter_slots.len()`) are all `-1`, which is not the
97/// attention `seq_slot` convention (there `-1` means the active adapter).
98/// Model-engine uploads it in `upload_moe_row_adapter`.
99pub fn build_moe_row_adapter_decode(
100 adapter_slots: &[i32],
101 padded_n: usize,
102 active: i32,
103 has_moe_lora: bool,
104) -> Vec<i32> {
105 (0..padded_n)
106 .map(|i| match adapter_slots.get(i).copied() {
107 Some(slot) => match resolve_moe_lora_route(slot, active, has_moe_lora) {
108 MoeLoraRoute::Fold => active,
109 MoeLoraRoute::Skip | MoeLoraRoute::Refuse => -1,
110 },
111 None => -1,
112 })
113 .collect()
114}
115
116#[cfg(test)]
117#[path = "moe_row_adapter_tests.rs"]
118mod tests;