1use anyhow::{Context, Result};
19use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
20
21use super::ids::{PleIdDims, ple_ngram_ids};
22use crate::layer::ForwardContext;
23use crate::layers::ngram_embed::NgramTable;
24use crate::layers::ops;
25use crate::weight_map::DenseWeight;
26
27pub struct PleSeqState {
30 conv: DevicePtr,
32 history: Vec<u32>,
34 prestaged_va: Option<u64>,
38 last_staged_va: u64,
43}
44
45pub struct PleLayer {
46 dims: PleIdDims,
47 head_dim: usize,
48 hidden: usize,
49 hc_mult: usize,
50 state_len: usize,
51 k_size: usize,
52 dilation: usize,
53 eps: f32,
54
55 key_proj: DenseWeight,
56 value_proj: DenseWeight,
57 norm_key: DenseWeight,
58 norm_query: DenseWeight,
59 norm_conv: DenseWeight,
60 conv1d: DenseWeight,
61 table: std::sync::Mutex<NgramTable>,
65
66 embed_k: KernelHandle,
67 gemm_k: KernelHandle,
68 gate_k: KernelHandle,
69 conv_k: KernelHandle,
70 add_k: KernelHandle,
71
72 emb: DevicePtr,
74 key: DevicePtr,
75 value: DevicePtr,
76 gated: DevicePtr,
77 gated_normed: DevicePtr,
78 out: DevicePtr,
79 slots_dev: DevicePtr,
80 max_tokens: usize,
81}
82
83impl PleLayer {
84 #[allow(clippy::too_many_arguments)]
85 pub fn new(
86 dims: PleIdDims,
87 head_dim: usize,
88 hidden: usize,
89 hc_mult: usize,
90 k_size: usize,
91 dilation: usize,
92 eps: f32,
93 weights: PleWeights,
94 table: NgramTable,
95 max_tokens: usize,
96 gpu: &dyn GpuBackend,
97 ) -> Result<Self> {
98 dims.validate()?;
99 let heads = dims.ngram_heads();
100 anyhow::ensure!(
101 heads * head_dim == hidden,
102 "PLE: {heads} heads x {head_dim} dims = {} != ple_embed_dim {hidden}. \
103 The head slices are CONCATENATED (not summed as LongCat's are), so \
104 this product is the embedding width and a mismatch means the \
105 geometry is not what we think.",
106 heads * head_dim
107 );
108 let c = hc_mult * hidden;
109 let state_len = (k_size - 1) * dilation;
110 Ok(Self {
111 dims,
112 head_dim,
113 hidden,
114 hc_mult,
115 state_len,
116 k_size,
117 dilation,
118 eps,
119 key_proj: weights.key_proj,
120 value_proj: weights.value_proj,
121 norm_key: weights.norm_key,
122 norm_query: weights.norm_query,
123 norm_conv: weights.norm_conv,
124 conv1d: weights.conv1d,
125 table: std::sync::Mutex::new(table),
126 embed_k: gpu.kernel("embed_from_argmax", "batched_embed")?,
127 gemm_k: gpu.kernel("gemm", "dense_gemm_bf16_pipelined")?,
128 gate_k: gpu.kernel("ple", "ple_gate")?,
129 conv_k: gpu.kernel("ple", "ple_conv")?,
130 add_k: gpu.kernel("ple", "ple_add_highway")?,
131 emb: gpu.alloc(max_tokens * hidden * 2)?,
132 key: gpu.alloc(max_tokens * c * 2)?,
133 value: gpu.alloc(max_tokens * hidden * 2)?,
134 gated: gpu.alloc(max_tokens * c * 4)?,
135 gated_normed: gpu.alloc(max_tokens * c * 4)?,
136 out: gpu.alloc(max_tokens * c * 4)?,
137 slots_dev: gpu.alloc(max_tokens * heads * 4)?,
138 max_tokens,
139 })
140 }
141
142 pub fn new_seq_state(&self, gpu: &dyn GpuBackend) -> Result<PleSeqState> {
146 Ok(PleSeqState {
147 conv: gpu.alloc(self.state_len * self.hc_mult * self.hidden * 4)?,
148 history: Vec::new(),
149 prestaged_va: None,
150 last_staged_va: 0,
151 })
152 }
153
154 pub fn rearm(&self, st: &mut PleSeqState) {
161 if st.last_staged_va != 0 {
162 st.prestaged_va = Some(st.last_staged_va);
163 }
164 }
165
166 pub fn forward_row(
170 &self,
171 st: &mut PleSeqState,
172 highway_row: DevicePtr,
173 ids: &[u32],
174 ctx: &ForwardContext,
175 stream: u64,
176 ) -> Result<()> {
177 self.forward_with_ids(st, highway_row, 1, false, Some(ids), ctx, stream)
178 }
179
180 pub fn forward(
183 &self,
184 st: &mut PleSeqState,
185 highway: DevicePtr,
186 num_tokens: usize,
187 fresh: bool,
188 ctx: &ForwardContext,
189 stream: u64,
190 ) -> Result<()> {
191 self.forward_with_ids(st, highway, num_tokens, fresh, None, ctx, stream)
192 }
193
194 #[allow(clippy::too_many_arguments)]
195 fn forward_with_ids(
196 &self,
197 st: &mut PleSeqState,
198 highway: DevicePtr,
199 num_tokens: usize,
200 fresh: bool,
201 ids_override: Option<&[u32]>,
202 ctx: &ForwardContext,
203 stream: u64,
204 ) -> Result<()> {
205 anyhow::ensure!(
206 num_tokens <= self.max_tokens,
207 "PLE: {num_tokens} tokens exceeds the {} this layer was sized for. \
208 Raise METRALE_PLE_MAX_TOKENS (costs tokens*10240*14 bytes of \
209 scratch) or lower the prefill chunk size.",
210 self.max_tokens
211 );
212 let c = self.hc_mult * self.hidden;
213 let heads = self.dims.ngram_heads();
214 let gpu = ctx.gpu;
215
216 let tokens: Vec<u32> = if let Some(ov) = ids_override {
221 anyhow::ensure!(ov.len() == num_tokens, "PLE: ids_override length");
222 ov.to_vec()
223 } else if let Some(host) = ctx.host_token_ids {
224 anyhow::ensure!(
225 host.len() >= num_tokens,
226 "PLE: host_token_ids has {} ids for {num_tokens} tokens",
227 host.len()
228 );
229 host[..num_tokens].to_vec()
230 } else {
231 anyhow::ensure!(
234 !ctx.graph_capture,
235 "PLE: no host_token_ids and a D2H readback is \
236 capture-unsupported; thread the host ids through this pass"
237 );
238 let tok_dev = ctx.token_ids.ok_or_else(|| {
239 anyhow::anyhow!(
240 "PLE needs token ids (host or device); this pass staged \
241 neither"
242 )
243 })?;
244 let mut raw = vec![0u8; num_tokens * 4];
245 gpu.copy_d2h(tok_dev, &mut raw)?;
246 raw.chunks_exact(4)
247 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
248 .collect()
249 };
250
251 let prestaged = st
258 .prestaged_va
259 .take()
260 .filter(|_| num_tokens == 1 && !fresh && ids_override.is_none());
261 if fresh || st.history.len() != self.dims.context_len() {
262 self.reset(st, gpu, stream)?;
263 }
264
265 if let Some(table_va) = prestaged {
266 self.gather_embed(table_va, num_tokens, heads, gpu, stream)?;
270 } else {
271 anyhow::ensure!(
272 !ctx.graph_capture,
273 "PLE: un-prestaged forward inside CUDA graph capture — the \
274 pageable slot upload would invalidate the recording (901); \
275 the scheduler must call decode_prestage every step"
276 );
277 let mut window = st.history.clone();
281 window.extend_from_slice(&tokens);
282 let all = ple_ngram_ids(&self.dims, &window);
283 let rows = &all[all.len() - num_tokens..];
284 let flat: Vec<u64> = rows.iter().flat_map(|r| r.iter().copied()).collect();
285
286 self.gather(&flat, num_tokens, heads, gpu, stream)?;
287
288 let keep = self.dims.context_len();
289 st.history = window[window.len() - keep..].to_vec();
290 }
291
292 ops::dense_gemm_bf16_pipelined(
298 gpu,
299 self.gemm_k,
300 self.emb,
301 &self.key_proj,
302 self.key,
303 num_tokens as u32,
304 c as u32,
305 self.hidden as u32,
306 stream,
307 )
308 .context("PLE key_proj")?;
309 ops::dense_gemm_bf16_pipelined(
310 gpu,
311 self.gemm_k,
312 self.emb,
313 &self.value_proj,
314 self.value,
315 num_tokens as u32,
316 self.hidden as u32,
317 self.hidden as u32,
318 stream,
319 )
320 .context("PLE value_proj")?;
321
322 ops::ple_gate(
323 gpu,
324 self.gate_k,
325 highway,
326 self.key,
327 self.value,
328 self.norm_query.weight,
329 self.norm_key.weight,
330 self.norm_conv.weight,
331 self.gated,
332 self.gated_normed,
333 num_tokens as u32,
334 self.hidden as u32,
335 self.hc_mult as u32,
336 self.eps,
337 stream,
338 )?;
339 ops::ple_conv(
340 gpu,
341 self.conv_k,
342 self.gated_normed,
343 self.gated,
344 self.conv1d.weight,
345 st.conv,
346 self.out,
347 num_tokens as u32,
348 c as u32,
349 self.k_size as u32,
350 self.dilation as u32,
351 stream,
352 )?;
353 ops::ple_add_highway(
354 gpu,
355 self.add_k,
356 self.out,
357 highway,
358 (num_tokens * c) as u32,
359 stream,
360 )?;
361
362 Ok(())
363 }
364
365 fn gather(
371 &self,
372 ids: &[u64],
373 num_tokens: usize,
374 heads: usize,
375 gpu: &dyn GpuBackend,
376 stream: u64,
377 ) -> Result<()> {
378 let table_va = self.gather_host(ids, gpu, stream)?;
379 self.gather_embed(table_va, num_tokens, heads, gpu, stream)
380 }
381
382 fn gather_host(&self, ids: &[u64], gpu: &dyn GpuBackend, stream: u64) -> Result<u64> {
387 let mut table = self
388 .table
389 .lock()
390 .map_err(|_| anyhow::anyhow!("PLE table mutex poisoned"))?;
391 let table_va = match &mut *table {
392 #[cfg(feature = "cuda")]
393 NgramTable::Cached(cache) => {
394 let mut slots = Vec::with_capacity(ids.len());
398 let (h0, m0, _) = cache.stats();
399 let t0 = std::time::Instant::now();
400 cache.resolve(ids, &mut slots)?;
401 let (h1, m1, _) = cache.stats();
405 let (dh, dm) = (h1 - h0, m1 - m0);
406 let us = t0.elapsed().as_micros();
407 if ids.len() > 64 {
408 tracing::info!(
409 "PLE gather: {} ids, {dh} hits / {dm} misses, resolve {us}us",
410 ids.len()
411 );
412 } else {
413 tracing::debug!(
414 "PLE gather: {} ids, {dh} hits / {dm} misses, resolve {us}us",
415 ids.len()
416 );
417 }
418 let bytes: Vec<u8> = slots.iter().flat_map(|s| s.to_le_bytes()).collect();
419 gpu.copy_h2d_async(&bytes, self.slots_dev, stream)?;
420 let va = cache.table_dev_va()?;
421 cache.end_batch();
429 DevicePtr(va)
430 }
431 NgramTable::Bf16(w) => {
432 let bytes: Vec<u8> = ids.iter().flat_map(|r| (*r as u32).to_le_bytes()).collect();
435 gpu.copy_h2d_async(&bytes, self.slots_dev, stream)?;
436 w.weight
437 }
438 NgramTable::Fp8(_) => anyhow::bail!(
439 "PLE: FP8 n-gram tables are not wired. This checkpoint ships BF16 \
440 rows, which are both simpler and more accurate (on LongCat, BF16 \
441 measured 0.0050 error vs FP8's 0.0247)."
442 ),
443 };
444 Ok(table_va.0)
445 }
446
447 fn gather_embed(
450 &self,
451 table_va: u64,
452 num_tokens: usize,
453 heads: usize,
454 gpu: &dyn GpuBackend,
455 stream: u64,
456 ) -> Result<()> {
457 ops::batched_embed(
458 gpu,
459 self.embed_k,
460 self.slots_dev,
461 DevicePtr(table_va),
462 self.emb,
463 (num_tokens * heads) as u32,
464 self.head_dim as u32,
465 stream,
466 )
467 .context("PLE row gather")
468 }
469}
470
471pub struct PleWeights {
473 pub key_proj: DenseWeight,
474 pub value_proj: DenseWeight,
475 pub norm_key: DenseWeight,
476 pub norm_query: DenseWeight,
477 pub norm_conv: DenseWeight,
478 pub conv1d: DenseWeight,
479}
480
481#[path = "aux_state.rs"]
484mod aux_state;