1use anyhow::{Result, bail, ensure};
22use spark_runtime::gpu::{DevicePtr, GpuBackend};
23use spark_runtime::kernel_args::KernelLaunch;
24use spark_runtime::weights::expert_stream::{ExpertLru, ExpertSource};
25
26use super::{MoeV41, MoeV41LayerWeights, SLOT_TABLE_LAYERS, route_from_logits};
27
28const ROUTE_NR: usize = 384;
30const ROUTE_TOPK: usize = 6;
31
32fn device_route() -> bool {
33 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
34 *V.get_or_init(|| std::env::var("ATLAS_DS41_DEVICE_ROUTE").is_ok_and(|v| v == "1"))
35}
36
37fn device_route_diag() -> bool {
38 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
39 *V.get_or_init(|| std::env::var("ATLAS_DS41_DEVICE_ROUTE_DIAG").is_ok_and(|v| v == "1"))
40}
41
42static DIAG_CHECKED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
43
44impl MoeV41 {
45 pub fn upload_bias(gpu: &dyn GpuBackend, bias: &[f32]) -> Result<DevicePtr> {
47 let bytes: Vec<u8> = bias.iter().flat_map(|v| v.to_le_bytes()).collect();
48 let p = gpu.alloc(bytes.len().max(16))?;
49 gpu.copy_h2d(&bytes, p)?;
50 Ok(p)
51 }
52
53 pub(super) fn device_route_ok(&self, w: &MoeV41LayerWeights) -> bool {
55 if !device_route() {
56 return false;
57 }
58 let c = &self.cfg;
59 let fits = c.n_routed == ROUTE_NR
60 && c.topk == ROUTE_TOPK
61 && (w.layer as usize) < SLOT_TABLE_LAYERS
62 && !w.gate_bias_dev.is_null();
63 if !fits {
64 static ONCE: std::sync::Once = std::sync::Once::new();
65 ONCE.call_once(|| {
66 tracing::warn!(
67 "moe_v41: ATLAS_DS41_DEVICE_ROUTE=1 ignored (n_routed {} topk {} layer {}): the kernel is written for {ROUTE_NR} x {ROUTE_TOPK}",
68 c.n_routed,
69 c.topk,
70 w.layer
71 );
72 });
73 }
74 fits
75 }
76
77 pub fn header_words(&self) -> usize {
80 1 + 3 * self.cfg.topk
81 }
82
83 fn header_of(&self, layer: u32) -> DevicePtr {
85 DevicePtr(self.route_hdr.0 + (layer as usize * self.header_words() * 4) as u64)
86 }
87
88 pub fn arena_of(lru: &ExpertLru) -> (u64, u64, u64, u64, u64) {
90 let lay = lru.layout();
91 (
92 lru.arena_dev().0,
93 lay.bytes as u64,
94 lay.gate_off as u64,
95 lay.up_off as u64,
96 lay.down_off as u64,
97 )
98 }
99
100 pub fn route_select_deferred(
105 &self,
106 gpu: &dyn GpuBackend,
107 w: &MoeV41LayerWeights,
108 arena: (u64, u64, u64, u64, u64),
109 stream: u64,
110 ) -> Result<()> {
111 ensure!(
112 self.device_route_ok(w),
113 "moe_v41: the step graph needs ATLAS_DS41_DEVICE_ROUTE=1 (layer {})",
114 w.layer
115 );
116 self.route_select_launch(gpu, w.layer, w.gate_bias_dev, arena, stream)
117 }
118
119 pub fn read_headers(&self, gpu: &dyn GpuBackend, stream: u64) -> Result<Vec<u8>> {
121 let mut hdr = vec![0u8; SLOT_TABLE_LAYERS * self.header_words() * 4];
122 gpu.copy_d2h_on_stream(self.route_hdr, &mut hdr, stream)?;
123 Ok(hdr)
124 }
125
126 pub fn parse_header(&self, hdr: &[u8], layer: u32) -> Result<(bool, Vec<usize>, Vec<f32>)> {
128 let k = self.cfg.topk;
129 let base = layer as usize * self.header_words();
130 let word = |i: usize| {
131 let o = 4 * (base + i);
132 i32::from_le_bytes([hdr[o], hdr[o + 1], hdr[o + 2], hdr[o + 3]])
133 };
134 let picks: Vec<usize> = (0..k).map(|i| word(1 + i) as usize).collect();
135 ensure!(
136 picks.iter().all(|&e| e < self.cfg.n_routed),
137 "device route L{layer}: pick outside 0..{}",
138 self.cfg.n_routed
139 );
140 let weights = (0..k)
141 .map(|i| f32::from_bits(word(1 + k + i) as u32))
142 .collect();
143 Ok((word(0) != 0, picks, weights))
144 }
145
146 pub(super) fn route_select_launch(
151 &self,
152 gpu: &dyn GpuBackend,
153 layer: u32,
154 bias_dev: DevicePtr,
155 arena: (u64, u64, u64, u64, u64),
156 stream: u64,
157 ) -> Result<()> {
158 let c = &self.cfg;
159 let row = DevicePtr(self.slot_table.0 + (layer as usize * c.n_routed * 4) as u64);
160 KernelLaunch::new(gpu, self.k.route_select)
161 .grid([1, 1, 1])
162 .block([ROUTE_NR as u32, 1, 1])
163 .arg_ptr(self.logits)
164 .arg_ptr(bias_dev)
165 .arg_f32(c.gate_temp)
166 .arg_f32(c.route_scale)
167 .arg_u32(c.norm_topk_prob as u32)
168 .arg_ptr(row)
169 .arg_u64(arena.0)
170 .arg_u64(arena.1)
171 .arg_u64(arena.2)
172 .arg_u64(arena.3)
173 .arg_u64(arena.4)
174 .arg_ptr(self.header_of(layer))
175 .arg_ptr(self.weight_dev)
176 .arg_ptr(self.rows_dev)
177 .arg_ptr(self.ptrs_dev)
178 .launch(stream)
179 }
180
181 pub(crate) fn slot_table_update(
183 &self,
184 gpu: &dyn GpuBackend,
185 changes: &[(u32, u32, i32)],
186 stream: u64,
187 ) -> Result<()> {
188 if changes.is_empty() {
189 return Ok(());
190 }
191 let c = &self.cfg;
192 let mut last: std::collections::HashMap<(u32, u32), i32> =
197 std::collections::HashMap::with_capacity(changes.len());
198 for &(l, e, s) in changes {
199 last.insert((l, e), s);
200 }
201 let changes: Vec<(u32, u32, i32)> = last.into_iter().map(|((l, e), s)| (l, e, s)).collect();
202 let per = (c.max_tokens * c.dim * 2 / 12).max(1);
205 for chunk in changes.chunks(per) {
206 let mut bytes: Vec<u8> = Vec::with_capacity(chunk.len() * 12);
207 for &(l, e, s) in chunk {
208 ensure!(
209 (l as usize) < SLOT_TABLE_LAYERS && (e as usize) < c.n_routed,
210 "slot table: layer {l} expert {e} outside the table"
211 );
212 bytes.extend_from_slice(&(l as i32).to_le_bytes());
213 bytes.extend_from_slice(&(e as i32).to_le_bytes());
214 bytes.extend_from_slice(&s.to_le_bytes());
215 }
216 gpu.copy_h2d_async(&bytes, self.a_rows, stream)?;
217 let n = chunk.len() as u32;
218 KernelLaunch::new(gpu, self.k.slot_table_set)
219 .grid([n.div_ceil(256), 1, 1])
220 .block([256, 1, 1])
221 .arg_ptr(self.slot_table)
222 .arg_u32(c.n_routed as u32)
223 .arg_ptr(self.a_rows)
224 .arg_u32(n)
225 .launch(stream)?;
226 gpu.synchronize(stream)?;
230 }
231 Ok(())
232 }
233
234 pub(super) fn route_device_m1<S: ExpertSource + ?Sized>(
239 &self,
240 gpu: &dyn GpuBackend,
241 w: &MoeV41LayerWeights,
242 lru: &mut ExpertLru,
243 src: &S,
244 reader_threads: usize,
245 stream: u64,
246 ) -> Result<(Vec<f32>, Vec<usize>)> {
247 let c = &self.cfg;
248 let arena = Self::arena_of(lru);
249 let pending = lru.drain_slot_changes();
252 self.slot_table_update(gpu, &pending, stream)?;
253 self.route_select_launch(gpu, w.layer, w.gate_bias_dev, arena, stream)?;
254 let k = c.topk;
255 let mut hdr = vec![0u8; (1 + 3 * k) * 4];
256 gpu.copy_d2h_on_stream(self.header_of(w.layer), &mut hdr, stream)?;
257 let (miss_flag, indices, weights) = self.parse_header(&hdr, 0)?;
258 lru.begin_token();
261 let keys: Vec<(u32, u32)> = indices.iter().map(|&e| (w.layer, e as u32)).collect();
262 let slots = lru.fetch_many_on(gpu, stream, src, &keys, &[], reader_threads)?;
263 let changes = lru.drain_slot_changes();
264 let changed = !changes.is_empty();
265 self.slot_table_update(gpu, &changes, stream)?;
266 if miss_flag || changed {
267 let (_, _, plan) = self.plan(&indices, &weights, 1);
271 ensure!(
272 plan.len() == k,
273 "device route: {} distinct picks, expected {k}",
274 plan.len()
275 );
276 self.upload_expert_table(gpu, &plan, &slots, stream)?;
277 }
278 if device_route_diag() {
279 self.device_route_check(gpu, w, &indices, &weights, &slots, stream)?;
280 }
281 Ok((weights, indices))
282 }
283
284 fn device_route_check(
287 &self,
288 gpu: &dyn GpuBackend,
289 w: &MoeV41LayerWeights,
290 indices: &[usize],
291 weights: &[f32],
292 slots: &[spark_runtime::weights::expert_stream::ExpertSlot],
293 stream: u64,
294 ) -> Result<()> {
295 let c = &self.cfg;
296 let k = c.topk;
297 let mut bytes = vec![0u8; c.n_routed * 4];
298 gpu.copy_d2h_on_stream(self.logits, &mut bytes, stream)?;
299 let logits: Vec<f32> = bytes
300 .chunks_exact(4)
301 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
302 .collect();
303 let (hw, hi) = route_from_logits(&logits, 1, &w.gate_bias, c);
304 let l = w.layer;
305 if hi != indices {
306 bail!("device route L{l}: picks {indices:?} != host {hi:?}");
307 }
308 let (wb, hwb): (Vec<u32>, Vec<u32>) = (
309 weights.iter().map(|v| v.to_bits()).collect(),
310 hw.iter().map(|v| v.to_bits()).collect(),
311 );
312 if wb != hwb {
313 bail!("device route L{l}: weight bits {wb:08x?} != host {hwb:08x?}");
314 }
315 let (_, w_host, plan) = self.plan(&hi, &hw, 1);
317 let mut w_dev = vec![0u8; k * 4];
318 gpu.copy_d2h_on_stream(self.weight_dev, &mut w_dev, stream)?;
319 if w_dev != w_host {
320 bail!("device route L{l}: plan weights differ from the host plan");
321 }
322 let mut ptrs = vec![0u8; 3 * k * 8];
323 gpu.copy_d2h_on_stream(self.ptrs_dev, &mut ptrs, stream)?;
324 let mut want: Vec<u8> = Vec::with_capacity(3 * k * 8);
325 for which in 0..3 {
326 for &(a0, _, _) in &plan {
327 let s = slots[a0];
328 want.extend_from_slice(&[s.gate, s.up, s.down][which].0.to_le_bytes());
329 }
330 }
331 if ptrs != want {
332 bail!("device route L{l}: pointer table differs from the fetched slots");
333 }
334 let n = DIAG_CHECKED.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
335 if n.is_multiple_of(4000) {
336 tracing::info!("device route diag: {n} layer-tokens checked, 0 mismatches");
337 }
338 Ok(())
339 }
340}