1use anyhow::{Context, Result, bail};
17use half::bf16;
18use std::ffi::c_void;
19
20use crate::cuda_min::{
21 CudaCtx, CudaModule, DeviceBuffer, copy_d_to_h_async, copy_h_to_d_async, launch_kernel,
22 stream_sync,
23};
24use crate::projection::{PredictorShape, build_projection};
25
26include!(concat!(env!("OUT_DIR"), "/storage_ptx.rs"));
27
28#[derive(Clone, Copy, Debug)]
29pub struct PredictorDims {
30 pub num_layers: usize,
31 pub num_q_heads: usize,
32 pub num_kv_heads: usize,
33 pub head_dim: usize,
34 pub r: usize,
35 pub block_size: usize,
36 pub max_blocks: usize,
37}
38
39impl PredictorDims {
40 pub fn validate(&self) -> Result<()> {
41 if !self.num_q_heads.is_multiple_of(self.num_kv_heads) {
42 bail!(
43 "num_q_heads ({}) must divide num_kv_heads ({})",
44 self.num_q_heads,
45 self.num_kv_heads
46 );
47 }
48 Ok(())
49 }
50 pub fn gqa_ratio(&self) -> i32 {
51 (self.num_q_heads / self.num_kv_heads) as i32
52 }
53 pub fn a_g_bytes(&self) -> usize {
54 self.num_layers * self.max_blocks * self.num_kv_heads * self.block_size * self.r * 2
57 }
58 pub fn per_layer_block_floats(&self) -> usize {
59 self.num_kv_heads * self.block_size * self.r
60 }
61 pub fn p_bytes(&self) -> usize {
62 self.head_dim * self.r * 2
63 }
64}
65
66pub struct Predictor {
67 dims: PredictorDims,
68 _modules: Vec<CudaModule>,
69 f_q_proj: u64,
70 f_kv_proj: u64,
71 f_score: u64,
72 p_dev: DeviceBuffer,
74 a_g_dev: DeviceBuffer,
76}
77
78impl Predictor {
79 pub fn new(ctx: &CudaCtx, dims: PredictorDims, projection_seed: u64) -> Result<Self> {
80 Self::new_on_stream(ctx.stream, dims, projection_seed)
81 }
82
83 pub fn new_on_stream(stream: u64, dims: PredictorDims, projection_seed: u64) -> Result<Self> {
84 dims.validate()?;
85 let mut modules: Vec<CudaModule> = Vec::new();
88 let mut f_q_proj = 0u64;
89 let mut f_kv_proj = 0u64;
90 let mut f_score = 0u64;
91 for entry in STORAGE_PTX.iter() {
92 match entry.name {
93 "q_lowrank_project" | "kv_lowrank_project" | "predictor_score" => {
94 let m = CudaModule::from_ptx(entry.ptx)
95 .with_context(|| format!("load PTX module {}", entry.name))?;
96 match entry.name {
97 "q_lowrank_project" => f_q_proj = m.function("q_lowrank_project")?,
98 "kv_lowrank_project" => f_kv_proj = m.function("kv_lowrank_project")?,
99 "predictor_score" => f_score = m.function("predictor_score")?,
100 _ => unreachable!(),
101 }
102 modules.push(m);
103 }
104 _ => {}
105 }
106 }
107 if f_q_proj == 0 || f_kv_proj == 0 || f_score == 0 {
108 bail!("missing predictor kernel function — PTX list incomplete");
109 }
110 let shape = PredictorShape::new(dims.head_dim, dims.r);
111 let p_host = build_projection(shape, projection_seed);
112 let p_dev = DeviceBuffer::new(dims.p_bytes())?;
113 copy_h_to_d_async(
114 p_dev.ptr,
115 p_host.as_ptr() as *const c_void,
116 dims.p_bytes(),
117 stream,
118 )?;
119 let a_g_need = dims.a_g_bytes();
123 let (free_hbm, _total_hbm) = crate::cuda_min::mem_info()?;
124 let a_g_budget = free_hbm.saturating_mul(95) / 100;
127 if a_g_need > a_g_budget {
128 bail!(
129 "HSS predictor A_g would need {:.2} GB but only {:.2} GB of HBM is free \
130 (5% margin reserved for scratch + tiled-attention).\n\
131 Tune one of:\n \
132 - --high-speed-swap-rank: current {} ; try {} (halves A_g)\n \
133 - --max-seq-len: max_blocks={} → currently dominates A_g; halve --max-seq-len to halve A_g\n \
134 - --kv-cache-dtype nvfp4: halves the KV pool, freeing room for A_g\n \
135 - --gpu-memory-utilization: lower so weight-side allocations leave more HBM\n\
136 A_g sizing = num_layers ({}) × max_blocks ({}) × num_kv_heads ({}) × block_size ({}) × r ({}) × 2 bytes.",
137 a_g_need as f64 / (1u64 << 30) as f64,
138 a_g_budget as f64 / (1u64 << 30) as f64,
139 dims.r,
140 dims.r / 2,
141 dims.max_blocks,
142 dims.num_layers,
143 dims.max_blocks,
144 dims.num_kv_heads,
145 dims.block_size,
146 dims.r,
147 );
148 }
149 let a_g_dev = DeviceBuffer::new(a_g_need)?;
153 stream_sync(stream)?;
154 Ok(Self {
155 dims,
156 _modules: modules,
157 f_q_proj,
158 f_kv_proj,
159 f_score,
160 p_dev,
161 a_g_dev,
162 })
163 }
164
165 pub fn project_q(&self, ctx: &CudaCtx, q: u64, q_proj: u64) -> Result<()> {
168 self.project_q_on_stream(ctx.stream, q, q_proj)
169 }
170
171 pub fn project_q_on_stream(&self, stream: u64, q: u64, q_proj: u64) -> Result<()> {
173 let mut q_v = q;
174 let mut p_v = self.p_dev.ptr;
175 let mut o_v = q_proj;
176 let mut nq = self.dims.num_q_heads as i32;
177 let mut hd = self.dims.head_dim as i32;
178 let mut r = self.dims.r as i32;
179 let mut params = [
180 &mut q_v as *mut _ as *mut c_void,
181 &mut p_v as *mut _ as *mut c_void,
182 &mut o_v as *mut _ as *mut c_void,
183 &mut nq as *mut _ as *mut c_void,
184 &mut hd as *mut _ as *mut c_void,
185 &mut r as *mut _ as *mut c_void,
186 ];
187 launch_kernel(
188 self.f_q_proj,
189 (self.dims.num_q_heads as u32, 1, 1),
190 (self.dims.r as u32, 1, 1),
191 0,
192 stream,
193 &mut params,
194 )
195 }
196
197 pub fn project_kv_block(
201 &self,
202 ctx: &CudaCtx,
203 layer: usize,
204 block_id: usize,
205 k_block: u64,
206 ) -> Result<()> {
207 self.project_kv_block_on_stream(ctx.stream, layer, block_id, k_block)
208 }
209
210 pub fn project_kv_block_on_stream(
211 &self,
212 stream: u64,
213 layer: usize,
214 block_id: usize,
215 k_block: u64,
216 ) -> Result<()> {
217 if layer >= self.dims.num_layers || block_id >= self.dims.max_blocks {
218 bail!("project_kv_block out of range: layer {layer}, block {block_id}");
219 }
220 let slot_floats = self.dims.per_layer_block_floats();
221 let k_lr_slot = self.a_g_dev.ptr
222 + (((layer * self.dims.max_blocks + block_id) * slot_floats) * 2) as u64;
223 let mut k_v = k_block;
224 let mut p_v = self.p_dev.ptr;
225 let mut o_v = k_lr_slot;
226 let mut bs = self.dims.block_size as i32;
227 let mut nk = self.dims.num_kv_heads as i32;
228 let mut hd = self.dims.head_dim as i32;
229 let mut r = self.dims.r as i32;
230 let mut params = [
231 &mut k_v as *mut _ as *mut c_void,
232 &mut p_v as *mut _ as *mut c_void,
233 &mut o_v as *mut _ as *mut c_void,
234 &mut bs as *mut _ as *mut c_void,
235 &mut nk as *mut _ as *mut c_void,
236 &mut hd as *mut _ as *mut c_void,
237 &mut r as *mut _ as *mut c_void,
238 ];
239 launch_kernel(
240 self.f_kv_proj,
241 (
242 self.dims.num_kv_heads as u32,
243 self.dims.block_size as u32,
244 1,
245 ),
246 (self.dims.r as u32, 1, 1),
247 0,
248 stream,
249 &mut params,
250 )
251 }
252
253 pub fn score_blocks(
257 &self,
258 ctx: &CudaCtx,
259 q_proj: u64,
260 k_lr_seq: u64,
261 scores_out: u64,
262 num_active_blocks: usize,
263 ) -> Result<()> {
264 self.score_blocks_on_stream(ctx.stream, q_proj, k_lr_seq, scores_out, num_active_blocks)
265 }
266
267 pub fn score_blocks_on_stream(
268 &self,
269 stream: u64,
270 q_proj: u64,
271 k_lr_seq: u64,
272 scores_out: u64,
273 num_active_blocks: usize,
274 ) -> Result<()> {
275 let mut q_v = q_proj;
276 let mut a_v = k_lr_seq;
277 let mut s_v = scores_out;
278 let mut nq = self.dims.num_q_heads as i32;
279 let mut nk = self.dims.num_kv_heads as i32;
280 let mut bs = self.dims.block_size as i32;
281 let mut r = self.dims.r as i32;
282 let mut gqa = self.dims.gqa_ratio();
283 let mut params = [
284 &mut q_v as *mut _ as *mut c_void,
285 &mut a_v as *mut _ as *mut c_void,
286 &mut s_v as *mut _ as *mut c_void,
287 &mut nq as *mut _ as *mut c_void,
288 &mut nk as *mut _ as *mut c_void,
289 &mut bs as *mut _ as *mut c_void,
290 &mut r as *mut _ as *mut c_void,
291 &mut gqa as *mut _ as *mut c_void,
292 ];
293 launch_kernel(
294 self.f_score,
295 (num_active_blocks as u32, 1, 1),
296 (128, 1, 1),
297 0,
298 stream,
299 &mut params,
300 )
301 }
302
303 pub fn dims(&self) -> PredictorDims {
304 self.dims
305 }
306 pub fn a_g_dev_ptr(&self) -> u64 {
307 self.a_g_dev.ptr
308 }
309}
310
311pub fn read_k_lr_slot(
314 ctx: &CudaCtx,
315 pred: &Predictor,
316 layer: usize,
317 block: usize,
318) -> Result<Vec<bf16>> {
319 let dims = pred.dims();
320 let n = dims.per_layer_block_floats();
321 let slot = pred.a_g_dev.ptr + (((layer * dims.max_blocks + block) * n) * 2) as u64;
322 let mut host = vec![bf16::from_f32(0.0); n];
323 copy_d_to_h_async(host.as_mut_ptr() as *mut c_void, slot, n * 2, ctx.stream)?;
324 stream_sync(ctx.stream)?;
325 Ok(host)
326}