1use anyhow::{Context, Result};
28use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
29
30use crate::layers::ops;
31
32#[path = "qsa_select.rs"]
33mod qsa_select;
34#[path = "qsa_snapshot.rs"]
35mod qsa_snapshot;
36#[cfg(test)]
37#[path = "qsa_tests.rs"]
38mod tests;
39
40pub struct QsaSelection {
42 pub k_scratch: DevicePtr,
43 pub v_scratch: DevicePtr,
44 pub table_dev: DevicePtr,
45 pub seq_len_dev: DevicePtr,
46 pub n_sel: u32,
47 pub max_blocks: u32,
48}
49
50pub struct QsaSeqState {
51 ingested: usize,
53 pooled: usize,
55 table_len: usize,
57 raw_keys: DevicePtr,
59 block_keys: DevicePtr,
61}
62
63pub struct QsaIndexer {
64 qk_proj_w: DevicePtr, q_norm_w: DevicePtr, k_norm_w: DevicePtr, n_heads: u32,
69 hd: u32,
70 ratio: u32,
71 budget: u32,
72 block_topk: u32,
73 rot: u32,
74 theta: f32,
75 eps: f32,
76 hidden: u32,
77 nkv_attn: u32,
78 hd_attn: u32,
79 max_tokens: usize,
80
81 k_pool_k: KernelHandle,
82 k_qprep_k: KernelHandle,
83 k_score_k: KernelHandle,
84 k_gather_k: KernelHandle,
85 k_qprep_rows_k: KernelHandle,
86 k_score_rows_k: KernelHandle,
87 k_score_rows_tc_k: KernelHandle,
90 k_prefill_attn_k: KernelHandle,
91
92 qk_scratch: DevicePtr, q_post: DevicePtr, scores_dev: DevicePtr, sel_dev: DevicePtr, k_scratch: DevicePtr, v_scratch: DevicePtr,
98 table_dev: DevicePtr, seq_len_dev: DevicePtr, prefill_table_dev: DevicePtr, }
105
106const INGEST_SLAB: usize = 2048;
108
109impl QsaIndexer {
110 #[allow(clippy::too_many_arguments)]
111 pub fn new(
112 qk_proj_w: DevicePtr,
113 q_norm_w: DevicePtr,
114 k_norm_w: DevicePtr,
115 n_heads: usize,
116 hd: usize,
117 ratio: usize,
118 budget: usize,
119 rot: usize,
120 theta: f32,
121 eps: f32,
122 hidden: usize,
123 nkv_attn: usize,
124 hd_attn: usize,
125 gpu: &dyn GpuBackend,
126 ) -> Result<Self> {
127 anyhow::ensure!(
128 ratio > 0 && budget.is_multiple_of(ratio),
129 "QSA: budget % ratio != 0"
130 );
131 let max_tokens: usize = std::env::var("ATLAS_QSA_MAX_TOKENS")
132 .ok()
133 .and_then(|v| v.parse().ok())
134 .unwrap_or(32768);
135 let block_topk = budget / ratio;
136 let qk_width = (n_heads + 1) * hd;
137 let sel_cap = budget + ratio;
138 Ok(Self {
139 qk_proj_w,
140 q_norm_w,
141 k_norm_w,
142 n_heads: n_heads as u32,
143 hd: hd as u32,
144 ratio: ratio as u32,
145 budget: budget as u32,
146 block_topk: block_topk as u32,
147 rot: rot as u32,
148 theta,
149 eps,
150 hidden: hidden as u32,
151 nkv_attn: nkv_attn as u32,
152 hd_attn: hd_attn as u32,
153 max_tokens,
154 k_pool_k: gpu.kernel("qsa_indexer", "qsa_block_pool")?,
155 k_qprep_k: gpu.kernel("qsa_indexer", "qsa_qprep")?,
156 k_score_k: gpu.kernel("qsa_indexer", "qsa_score")?,
157 k_gather_k: gpu.kernel("qsa_indexer", "qsa_gather")?,
158 k_qprep_rows_k: gpu.kernel("qsa_indexer", "qsa_qprep_rows")?,
159 k_score_rows_k: gpu.kernel("qsa_indexer", "qsa_score_rows")?,
160 k_score_rows_tc_k: crate::layers::try_kernel(gpu, "qsa_indexer", "qsa_score_rows_tc"),
161 k_prefill_attn_k: gpu.kernel("qsa_indexer", "qsa_prefill_attn")?,
162 qk_scratch: gpu.alloc(INGEST_SLAB * qk_width * 2)?,
163 q_post: gpu.alloc(n_heads * hd * 4)?,
164 scores_dev: gpu.alloc(max_tokens / ratio * 4)?,
165 sel_dev: gpu.alloc(sel_cap * 4)?,
166 k_scratch: gpu.alloc(sel_cap * nkv_attn * hd_attn * 2)?,
167 v_scratch: gpu.alloc(sel_cap * nkv_attn * hd_attn * 2)?,
168 table_dev: gpu.alloc(sel_cap.div_ceil(8) * 4)?,
169 seq_len_dev: gpu.alloc(4)?,
170 prefill_table_dev: gpu.alloc(max_tokens.div_ceil(8) * 4)?,
171 })
172 }
173
174 pub fn new_seq_state(&self, gpu: &dyn GpuBackend) -> Result<QsaSeqState> {
178 let hd = self.hd as usize;
179 let ratio = self.ratio as usize;
180 Ok(QsaSeqState {
181 ingested: 0,
182 pooled: 0,
183 table_len: 0,
184 raw_keys: gpu.alloc(self.max_tokens * hd * 2)?,
185 block_keys: gpu.alloc(self.max_tokens / ratio * hd * 2)?,
186 })
187 }
188
189 pub fn release_seq_state(&self, st: &mut QsaSeqState, gpu: &dyn GpuBackend) -> Result<()> {
204 let mut first_err: Option<anyhow::Error> = None;
205 for p in [&mut st.raw_keys, &mut st.block_keys] {
206 if p.is_null() {
207 continue;
208 }
209 if let Err(e) = gpu.free(*p) {
210 first_err.get_or_insert(e);
211 }
212 *p = DevicePtr(0);
213 }
214 match first_err {
215 Some(e) => Err(e),
216 None => Ok(()),
217 }
218 }
219
220 pub fn inert_bound(&self) -> usize {
221 (self.budget + self.ratio - 1) as usize
222 }
223
224 fn qk_width(&self) -> usize {
225 (self.n_heads as usize + 1) * self.hd as usize
226 }
227
228 pub fn prefill_ingest(
232 &self,
233 st: &mut QsaSeqState,
234 hidden: DevicePtr,
235 num_tokens: usize,
236 seq_start: usize,
237 gpu: &dyn GpuBackend,
238 stream: u64,
239 ) -> Result<()> {
240 if seq_start == 0 {
241 st.ingested = 0;
242 st.pooled = 0;
243 }
244 anyhow::ensure!(
245 seq_start == st.ingested,
246 "QSA: prefill chunk starts at {seq_start} but {} tokens are \
247 ingested — a prefix-cache skip bypassed the indexer. Serve \
248 qwen4_exp with the prefix cache disabled until QSA learns to \
249 re-ingest cached prefixes.",
250 st.ingested
251 );
252 anyhow::ensure!(
253 seq_start + num_tokens <= self.max_tokens,
254 "QSA: {} tokens exceeds ATLAS_QSA_MAX_TOKENS={}",
255 seq_start + num_tokens,
256 self.max_tokens
257 );
258
259 let hd = self.hd as usize;
260 let qkw = self.qk_width();
261 let mut off = 0usize;
262 while off < num_tokens {
263 let ts = INGEST_SLAB.min(num_tokens - off);
264 ops::cublas_bf16_proj_dense(
265 hidden.offset((off) * self.hidden as usize * 2),
266 self.qk_proj_w,
267 self.qk_scratch,
268 ts as u32,
269 qkw as u32,
270 self.hidden,
271 stream,
272 )
273 .context("QSA qk projection (prefill)")?;
274 gpu.copy_d2d_2d_async(
276 self.qk_scratch.offset(self.n_heads as usize * hd * 2),
277 qkw * 2,
278 st.raw_keys.offset((seq_start + off) * hd * 2),
279 hd * 2,
280 hd * 2,
281 ts,
282 stream,
283 )?;
284 off += ts;
285 }
286 st.ingested = seq_start + num_tokens;
287 self.pool_new_blocks(st, gpu, stream)
288 }
289
290 fn pool_new_blocks(
291 &self,
292 st: &mut QsaSeqState,
293 gpu: &dyn GpuBackend,
294 stream: u64,
295 ) -> Result<()> {
296 let complete = st.ingested / self.ratio as usize;
297 if complete > st.pooled {
298 ops::qsa_block_pool(
299 gpu,
300 self.k_pool_k,
301 st.raw_keys,
302 self.k_norm_w,
303 st.block_keys,
304 st.pooled as u32,
305 (complete - st.pooled) as u32,
306 self.ratio,
307 self.hd,
308 self.rot,
309 self.theta,
310 self.eps,
311 stream,
312 )?;
313 st.pooled = complete;
314 }
315 Ok(())
316 }
317
318 #[allow(clippy::too_many_arguments)]
323 pub fn decode_select(
324 &self,
325 st: &mut QsaSeqState,
326 normed: DevicePtr,
327 pos: usize,
328 k_pool: DevicePtr,
329 v_pool: DevicePtr,
330 block_table_dev: DevicePtr,
331 block_size: u32,
332 gpu: &dyn GpuBackend,
333 stream: u64,
334 ) -> Result<Option<QsaSelection>> {
335 anyhow::ensure!(
336 pos == st.ingested,
337 "QSA: decode at pos {pos} but {} tokens ingested — the indexer \
338 cache lost sync (prefix-cache skip or a rewound sequence)",
339 st.ingested
340 );
341 anyhow::ensure!(
342 pos < self.max_tokens,
343 "QSA: pos {pos} >= ATLAS_QSA_MAX_TOKENS"
344 );
345
346 let hd = self.hd as usize;
347 let qkw = self.qk_width();
348 ops::cublas_bf16_proj_dense(
350 normed,
351 self.qk_proj_w,
352 self.qk_scratch,
353 1,
354 qkw as u32,
355 self.hidden,
356 stream,
357 )
358 .context("QSA qk projection (decode)")?;
359 gpu.copy_d2d_async(
360 self.qk_scratch.offset(self.n_heads as usize * hd * 2),
361 st.raw_keys.offset(pos * hd * 2),
362 hd * 2,
363 stream,
364 )?;
365 st.ingested = pos + 1;
366 self.pool_new_blocks(st, gpu, stream)?;
367
368 let visible = pos + 1;
369 let complete = visible / self.ratio as usize;
370 if complete <= self.block_topk as usize {
371 return Ok(None); }
373
374 ops::qsa_qprep(
376 gpu,
377 self.k_qprep_k,
378 self.qk_scratch,
379 self.q_norm_w,
380 self.q_post,
381 self.n_heads,
382 self.hd,
383 self.rot,
384 pos as u32,
385 self.theta,
386 self.eps,
387 stream,
388 )?;
389 ops::qsa_score(
390 gpu,
391 self.k_score_k,
392 self.q_post,
393 st.block_keys,
394 self.scores_dev,
395 complete as u32,
396 self.n_heads,
397 self.hd,
398 stream,
399 )?;
400
401 let mut raw = vec![0u8; complete * 4];
404 gpu.copy_d2h_on_stream(self.scores_dev, &mut raw, stream)?;
405 let scores: Vec<f32> = raw
406 .chunks_exact(4)
407 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
408 .collect();
409 let mut order: Vec<u32> = (0..complete as u32).collect();
410 order.sort_by(|&a, &b| {
413 scores[b as usize]
414 .partial_cmp(&scores[a as usize])
415 .unwrap_or(std::cmp::Ordering::Equal)
416 .then(a.cmp(&b))
417 });
418 let mut blocks: Vec<u32> = order[..self.block_topk as usize].to_vec();
419 blocks.sort_unstable();
420
421 let ratio = self.ratio as usize;
422 let mut sel: Vec<i32> = Vec::with_capacity(self.budget as usize + ratio);
423 for b in &blocks {
424 let base = *b as i32 * self.ratio as i32;
425 for r in 0..self.ratio as i32 {
426 sel.push(base + r);
427 }
428 }
429 for t in complete * ratio..visible {
430 sel.push(t as i32);
431 }
432 let n_sel = sel.len() as u32;
433
434 let sel_bytes: Vec<u8> = sel.iter().flat_map(|v| v.to_le_bytes()).collect();
435 gpu.copy_h2d_async(&sel_bytes, self.sel_dev, stream)?;
436 ops::qsa_gather(
437 gpu,
438 self.k_gather_k,
439 k_pool,
440 v_pool,
441 block_table_dev,
442 self.sel_dev,
443 self.k_scratch,
444 self.v_scratch,
445 n_sel,
446 block_size,
447 self.nkv_attn,
448 self.hd_attn,
449 stream,
450 )?;
451
452 let pages = (n_sel as usize).div_ceil(block_size as usize);
454 if st.table_len < pages {
455 let ident: Vec<u8> = (0..pages as i32).flat_map(|v| v.to_le_bytes()).collect();
456 gpu.copy_h2d_async(&ident, self.table_dev, stream)?;
457 st.table_len = pages;
458 }
459 gpu.copy_h2d_async(&(n_sel as i32).to_le_bytes(), self.seq_len_dev, stream)?;
460
461 Ok(Some(QsaSelection {
462 k_scratch: self.k_scratch,
463 v_scratch: self.v_scratch,
464 table_dev: self.table_dev,
465 seq_len_dev: self.seq_len_dev,
466 n_sel,
467 max_blocks: pages as u32,
468 }))
469 }
470}