memra_engine/hyper.rs
1//! mHC — manifold-constrained hyper-connections, the `ResidualTopology::HyperConnections`
2//! residual program (glm5_next / GLM-5.3-Flash, and the dsv4 class).
3//!
4//! ARITHMETIC CONTRACT. Truth is `memra_reference::execute`'s `execute_hyper_layer`, which is
5//! itself built from `memra_gguf::dsv4_forward::{hc_expand, hc_pre, hc_post, hc_split_sinkhorn,
6//! hc_head}`. Every stage below cites the reference stage it reproduces. The vendor module the
7//! reference was derived from is
8//! `research/glm53-flash-bringup-20260827/modular_glm5_next-ref.py`.
9//!
10//! A trunk layer under this topology is NOT `x + attn; x += mlp`. Per site (attention, then
11//! MLP), with the stream state `x [tokens, streams, hidden]`:
12//!
13//! ```text
14//! mixes[t, :] = fn_w · x[t, :, :] (rows = (2+streams)*streams)
15//! mixes[t, :] *= rsqrt(mean(x[t]^2) + eps) (over the whole streams*hidden slab)
16//! pre/post/comb = sinkhorn(mixes[t, :], scale, base) (per token, per site)
17//! y[t, :] = Σ_c pre[t, c] · x[t, c, :] (collapse streams -> 1)
18//! f = branch(rms_norm(y)) (the mixer or the FFN, unchanged)
19//! x'[t, k, :] = post[t, k] · f[t, :] + Σ_j comb[t, j, k] · x[t, j, :]
20//! ```
21//!
22//! SINKHORN IS PER TOKEN AND PER SITE, NOT A LOAD-TIME PRECOMPUTE. `mixes` is
23//! `x @ fn_wᵀ` rescaled by the token's own RMS — an ACTIVATION, so the Sinkhorn normalization
24//! that turns it into `comb` cannot be hoisted to load even though the weights are static
25//! (`dsv4_forward.rs` `hc_pre`, the `matmul` + `rsq` block immediately before
26//! `hc_split_sinkhorn`). It runs on device, once per (token, layer, site).
27//!
28//! MEMORY LAYOUT: TOKEN-MAJOR `[tokens, streams, hidden]`, element `(t, k, i)` at
29//! `(t*streams + k)*hidden + i`. Forced, not chosen: it is the layout of `hc_expand` in the
30//! reference and of every kernel in the `memra_dsv4_hc_*` family, and it makes one token's
31//! `streams*hidden` slab contiguous — which is exactly the `[s, w]` operand the mixes GEMM and
32//! `memra_dsv4_rowsq_scale` want. Streams-major would have cost a transpose at both ends of
33//! every site. Any graph capture over these buffers sees one flat `t*streams*hidden` slab.
34//!
35//! KERNELS: no new math. `cu/dsv4_gpu.cu` already carries this exact program for the dsv4 GPU
36//! fork (`crate::dsv4_gpu`) and is compiled unconditionally into this crate, so the site mixing
37//! is `memra_dsv4_{rowsq_scale, hc_sinkhorn_m, hc_collapse, hc_post}` plus `hc_mean`/`hc_head_pre_m`
38//! at the exit, and the mixes GEMM is `Engine::linear` (cuBLASLt f32 — the tiny
39//! `[rows, streams*hidden]` operand is the wrong shape for the f64 island `dots` kernel the dsv4
40//! decode path uses, and this is a serving trunk, not a byte-parity oracle). The one kernel that
41//! did not exist, `memra_dsv4_hc_expand`, was added next to its inverse `memra_dsv4_hc_mean`.
42//! The `dsv4_` prefix is that translation unit's namespace, not a model claim — the reference
43//! reaches into `memra_gguf::dsv4_forward` for glm5_next in exactly the same way.
44//!
45//! NO ENV FLAG. The topology, its stream count, its epsilon, its Sinkhorn iteration count and
46//! its collapse are read from the compiled `ModelPlan`. There is nothing here to switch.
47
48use crate::Engine;
49use crate::dsv4_ffi as k;
50use crate::dsv4_ffi::ck;
51use crate::model::GpuTensor;
52use cudarc::driver::{CudaSlice, CudaStream, DevicePtr, DevicePtrMut};
53use memra_gguf::model_plan::{HcCollapse, ModelPlan, ResidualTopology};
54use memra_gguf::source::TensorSource;
55use std::os::raw::c_void;
56
57type Res<T> = Result<T, Box<dyn std::error::Error>>;
58
59fn sp(stream: &CudaStream) -> *mut c_void {
60 stream.cu_stream() as *mut c_void
61}
62
63macro_rules! dpf {
64 ($slice:expr, $stream:expr) => {{ $slice.device_ptr($stream).0 as *const f32 }};
65}
66macro_rules! dpm {
67 ($slice:expr, $stream:expr) => {{ $slice.device_ptr_mut($stream).0 as *mut f32 }};
68}
69
70/// The trunk-wide hyper-connection topology, read off the plan at load.
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct HyperTopology {
73 pub streams: usize,
74 pub epsilon: f32,
75 pub sinkhorn_iterations: u32,
76 pub collapse: HcCollapse,
77}
78
79impl HyperTopology {
80 /// `(2 + streams) * streams` — pre gates, post gates, then the `streams x streams`
81 /// combination block, in that row order (`hc_split_sinkhorn`).
82 pub fn rows(&self) -> usize {
83 (2 + self.streams) * self.streams
84 }
85
86 /// The plan's topology, or `None` for a serial/gemma trunk. Refuses a trunk whose layers
87 /// disagree: the state carried between layers is one shape, so a per-layer stream count is
88 /// not a thing this executor can mean. Mirrors `memra_reference`'s `hyper_topology`.
89 pub fn from_plan(plan: &ModelPlan) -> Result<Option<Self>, String> {
90 let mut found: Option<Self> = None;
91 for layer in &plan.layers {
92 let ResidualTopology::HyperConnections {
93 streams,
94 epsilon,
95 sinkhorn_iterations,
96 collapse,
97 } = layer.residual
98 else {
99 if found.is_some() {
100 return Err(format!(
101 "layer {} declares a serial/gemma residual while an earlier trunk layer \
102 declares HyperConnections; the topology must be uniform across the trunk",
103 layer.index
104 ));
105 }
106 continue;
107 };
108 let this = Self {
109 streams: streams as usize,
110 epsilon,
111 sinkhorn_iterations,
112 collapse,
113 };
114 if streams == 0 || epsilon <= 0.0 || sinkhorn_iterations == 0 {
115 return Err(format!(
116 "layer {}: HyperConnections need streams > 0, epsilon > 0 and \
117 sinkhorn_iterations > 0, got streams={streams} epsilon={epsilon} \
118 iterations={sinkhorn_iterations}",
119 layer.index
120 ));
121 }
122 match found {
123 None if layer.index != plan.layers[0].index => {
124 return Err(format!(
125 "layer {} declares HyperConnections but earlier trunk layers do not; the \
126 topology must be uniform across the trunk",
127 layer.index
128 ));
129 }
130 None => found = Some(this),
131 Some(first) if first != this => {
132 return Err(format!(
133 "layer {} declares {this:?} but the trunk opened with {first:?}; the \
134 topology must be uniform across the trunk",
135 layer.index
136 ));
137 }
138 Some(_) => {}
139 }
140 }
141 Ok(found)
142 }
143}
144
145/// One site's learned mixing parameters. `fn_w` is consumed as ROW-MAJOR `[rows,
146/// streams*hidden]` — the layout `memra_reference::hyper_set` and `dsv4_forward::HcSet` read, and
147/// the `[out_f, in_f]` operand `Engine::linear` wants. Only the element count is checked at load;
148/// the checkpoint dialect's `ne` ordering is not consulted, so the two readers cannot fork.
149pub struct HyperSite {
150 pub fn_w: CudaSlice<f32>,
151 pub base: CudaSlice<f32>,
152 pub scale: CudaSlice<f32>,
153}
154
155/// The six per-layer hc tensors, present iff the plan declares HyperConnections for the trunk.
156pub struct HyperLayer {
157 pub attn: HyperSite,
158 pub mlp: HyperSite,
159}
160
161/// Gated-head exit weights (`HcCollapse::GatedHead`, the dsv4 class). Absent under
162/// `HcCollapse::Mean`, which has no learned head (`Glm5NextTextHyperHead` is an unweighted mean).
163pub struct HyperHead {
164 pub fn_w: CudaSlice<f32>,
165 pub base: CudaSlice<f32>,
166 pub scale: CudaSlice<f32>,
167}
168
169/// A loaded float tensor's device data, or a refusal naming the tensor. `GpuTensor::float_data`
170/// panics on the quantized/bf16 variants; an hc parameter arriving in one of those is a
171/// checkpoint the trunk cannot serve, and it must say which tensor and why.
172fn float_data<'a>(name: &str, t: &'a GpuTensor, want: usize) -> Result<&'a CudaSlice<f32>, String> {
173 let data = match t {
174 GpuTensor::Float { data, .. } => data,
175 GpuTensor::Quant { .. } => {
176 return Err(format!(
177 "{name}: hyper-connection parameters must be f32-resident, got a quantized \
178 tensor; re-mint this tensor unquantized (the whole hc program is an f32 island)"
179 ));
180 }
181 GpuTensor::FloatBf16 { .. } => {
182 return Err(format!(
183 "{name}: hyper-connection parameters must be f32-resident, got a bf16-resident \
184 matmul weight"
185 ));
186 }
187 };
188 if data.len() != want {
189 return Err(format!(
190 "{name}: {} elements, the plan's HyperConnections require {want}",
191 data.len()
192 ));
193 }
194 Ok(data)
195}
196
197/// Load one site's trio, refusing loudly — by name — on the first absent tensor. There is no
198/// serial fallback: a plan that declares HyperConnections and a checkpoint that does not carry
199/// them describe two different functions, and guessing which one to compute is the failure this
200/// refusal exists to prevent.
201fn load_site(
202 e: &Engine,
203 src: &dyn TensorSource,
204 il: u32,
205 topology: &HyperTopology,
206 hidden: usize,
207 site: &str,
208) -> Res<HyperSite> {
209 let rows = topology.rows();
210 let width = topology.streams * hidden;
211 let mut out: Vec<CudaSlice<f32>> = Vec::with_capacity(3);
212 for (suffix, want) in [
213 ("fn", rows * width),
214 ("base", rows),
215 // Three gate scales — pre, post, combination — regardless of stream count
216 // (`hc_split_sinkhorn` asserts `scale.len() == 3`).
217 ("scale", 3),
218 ] {
219 // The ggml spellings `add_hyper_connections` (memra-gguf tensor_contract) emits.
220 let name = format!("blk.{il}.{site}_{suffix}");
221 if !src.has(&name) {
222 return Err(format!(
223 "{name} is absent, but the compiled ModelPlan declares \
224 ResidualTopology::HyperConnections{{ streams: {} }} for layer {il}. Refusing to \
225 load: a serial residual would compute a different model, silently.",
226 topology.streams
227 )
228 .into());
229 }
230 let loaded = GpuTensor::load_from_source(e, src, &name)?;
231 out.push(e.clone_dtod(float_data(&name, &loaded, want)?)?);
232 }
233 let mut out = out.into_iter();
234 Ok(HyperSite {
235 fn_w: out.next().expect("function"),
236 base: out.next().expect("base"),
237 scale: out.next().expect("scale"),
238 })
239}
240
241impl HyperLayer {
242 pub fn load(
243 e: &Engine,
244 src: &dyn TensorSource,
245 il: u32,
246 topology: &HyperTopology,
247 hidden: usize,
248 ) -> Res<Self> {
249 Ok(Self {
250 attn: load_site(e, src, il, topology, hidden, "hc_attn")?,
251 mlp: load_site(e, src, il, topology, hidden, "hc_ffn")?,
252 })
253 }
254}
255
256impl HyperHead {
257 /// `None` unless the collapse is gated. `hc_head`'s trio is shaped differently from a site's:
258 /// `rows == streams` and one scale (`dsv4_forward::hc_head`).
259 pub fn load(
260 e: &Engine,
261 src: &dyn TensorSource,
262 topology: &HyperTopology,
263 hidden: usize,
264 ) -> Res<Option<Self>> {
265 if topology.collapse != HcCollapse::GatedHead {
266 return Ok(None);
267 }
268 let streams = topology.streams;
269 let mut out: Vec<CudaSlice<f32>> = Vec::with_capacity(3);
270 // The dsv4 checkpoint spellings (crate::dsv4_gpu's `hc_head_*` loads). The
271 // TensorContract has no HyperHead rows — nothing in the GGUF/safetensors schema emits
272 // them yet — so a gated-head trunk on THIS path refuses by name below until it does.
273 for (name, want) in [
274 ("hc_head_fn", streams * streams * hidden),
275 ("hc_head_base", streams),
276 ("hc_head_scale", 1),
277 ] {
278 if !src.has(name) {
279 return Err(format!(
280 "{name} is absent, but the compiled ModelPlan declares \
281 HcCollapse::GatedHead. Refusing to load: collapsing with an unweighted mean \
282 instead would compute a different model, silently."
283 )
284 .into());
285 }
286 let loaded = GpuTensor::load_from_source(e, src, name)?;
287 out.push(e.clone_dtod(float_data(name, &loaded, want)?)?);
288 }
289 let mut out = out.into_iter();
290 Ok(Some(Self {
291 fn_w: out.next().expect("function"),
292 base: out.next().expect("base"),
293 scale: out.next().expect("scale"),
294 }))
295 }
296}
297
298/// The per-token post gates and combination matrix a site's `hc_pre` produced, held for that
299/// site's `hc_post`. `post` is `[tokens, streams]`, `comb` is `[tokens, streams, streams]`.
300pub struct HcMix {
301 pub post: CudaSlice<f32>,
302 pub comb: CudaSlice<f32>,
303}
304
305/// Engagement counter for the fused pre-chain door (`MEMRA_HC_FUSED_PRE=1`): incremented at
306/// the arm's own call site, announced once per boot — the spec-engagement receipt the gate
307/// and any box A/B arm must show ([bf16-mmv] RESIDENT lesson: engagement lines are receipts,
308/// never inferred).
309pub static HC_FUSED_PRE_DISPATCHES: std::sync::atomic::AtomicU64 =
310 std::sync::atomic::AtomicU64::new(0);
311
312/// `MEMRA_HC_FUSED_PRE=1` (default OFF): the three-kernel site pre-chain (rowsq_scale +
313/// Sinkhorn + collapse) runs as ONE `memra_dsv4_hc_pre_fused` launch per site — bit-identical
314/// to the unfused chain by construction (verbatim bodies, asserted bytewise in
315/// `hc_fused_pre_gpu.rs`). Read PER CALL (the `MEMRA_MOE_FUSED_EPI` rollback-seam precedent),
316/// so both arms alternate inside one process and the flag is a live rollback seam.
317fn hc_fused_pre_on() -> bool {
318 std::env::var("MEMRA_HC_FUSED_PRE").as_deref() == Ok("1")
319}
320
321/// Model entry (`hc_expand`): `[tokens, hidden]` embeddings -> `[tokens, streams, hidden]`.
322pub fn expand(
323 e: &Engine,
324 topology: &HyperTopology,
325 embedded: &CudaSlice<f32>,
326 t: usize,
327 hidden: usize,
328) -> Res<CudaSlice<f32>> {
329 let streams = topology.streams;
330 let mut out = e.uninit(t * streams * hidden)?;
331 let stream = e.stream();
332 unsafe {
333 ck(
334 "hc_expand",
335 k::memra_dsv4_hc_expand(
336 dpf!(embedded, &stream),
337 dpm!(out, &stream),
338 t as i32,
339 streams as i32,
340 hidden as i32,
341 sp(&stream),
342 ),
343 )?;
344 }
345 Ok(out)
346}
347
348/// One site's pre-branch half (`hc_pre`): mixes GEMM, per-token RMS rescale, Sinkhorn, stream
349/// collapse. Returns the branch input `[tokens, hidden]` and the gates its `post` half needs.
350pub fn pre(
351 e: &Engine,
352 topology: &HyperTopology,
353 site: &HyperSite,
354 x: &CudaSlice<f32>,
355 t: usize,
356 hidden: usize,
357) -> Res<(CudaSlice<f32>, HcMix)> {
358 let width = topology.streams * hidden;
359 let mixes = e.linear(x, &site.fn_w, t, width, topology.rows())?;
360 pre_finish(e, topology, site, x, mixes, t, hidden)
361}
362
363/// `pre` with the DECODE-EXACT mixing GEMM: each token's mix coefficients come from the
364/// SAME m=1 cuBLASLt program the serial T=1 decode step runs (`linear_t1_into` is `linear`
365/// at m == 1 on a row view — same config, same weight pointer, same input bytes), instead
366/// of one m=t call whose n-dependent reduction split changes every output bit (the lt_ndep
367/// probe documented on `Engine::linear_decode_exact`). Everything after the GEMM is the
368/// per-token kernel set `pre` already runs — block-per-token programs whose per-token bytes
369/// do not depend on t. This is the entry the BATCHED hyper decode walk uses so that row b
370/// of a B-row tick is bit-identical to that session's solo `decode_step_hyper` step.
371pub fn pre_exact(
372 e: &Engine,
373 topology: &HyperTopology,
374 site: &HyperSite,
375 x: &CudaSlice<f32>,
376 t: usize,
377 hidden: usize,
378) -> Res<(CudaSlice<f32>, HcMix)> {
379 let rows = topology.rows();
380 let width = topology.streams * hidden;
381 let mut mixes = e.uninit(t * rows)?;
382 for r in 0..t {
383 let xr = x.slice(r * width..(r + 1) * width);
384 let wv = site.fn_w.slice(0..site.fn_w.len());
385 let mut yr = mixes.slice_mut(r * rows..(r + 1) * rows);
386 e.linear_t1_into(&xr, &wv, &mut yr, width, rows)
387 .map_err(|err| format!("hc pre_exact row {r}: {err}"))?;
388 }
389 pre_finish(e, topology, site, x, mixes, t, hidden)
390}
391
392/// The per-token half `pre` and `pre_exact` share: RMS rescale of the mix coefficients,
393/// Sinkhorn, stream collapse. Every kernel here is a block-per-token program (grid over t),
394/// so per-token output bytes are invariant to t — the two entries differ ONLY in how the
395/// mixes GEMM reduces.
396fn pre_finish(
397 e: &Engine,
398 topology: &HyperTopology,
399 site: &HyperSite,
400 x: &CudaSlice<f32>,
401 mut mixes: CudaSlice<f32>,
402 t: usize,
403 hidden: usize,
404) -> Res<(CudaSlice<f32>, HcMix)> {
405 let streams = topology.streams;
406 let mut pre_gates = e.uninit(t * streams)?;
407 let mut post = e.uninit(t * streams)?;
408 let mut comb = e.uninit(t * streams * streams)?;
409 let mut y = e.uninit(t * hidden)?;
410 pre_finish_into(
411 e,
412 topology,
413 site,
414 x,
415 &mut mixes,
416 &mut pre_gates,
417 &mut post,
418 &mut comb,
419 &mut y,
420 t,
421 hidden,
422 )?;
423 Ok((y, HcMix { post, comb }))
424}
425
426/// `pre_finish`'s kernel arms on caller-owned outputs — shared by the allocating entry above
427/// and the persistent-workspace decode walk (`pre_t1_ws`), so the two cannot drift. Both arms
428/// fully overwrite every output element, which is what makes workspace reuse byte-identical.
429#[allow(clippy::too_many_arguments)] // allow: the parameter list mirrors the kernel/FFI contract; the workspace caller passes disjoint field borrows
430fn pre_finish_into(
431 e: &Engine,
432 topology: &HyperTopology,
433 site: &HyperSite,
434 x: &CudaSlice<f32>,
435 mixes: &mut CudaSlice<f32>,
436 pre_gates: &mut CudaSlice<f32>,
437 post: &mut CudaSlice<f32>,
438 comb: &mut CudaSlice<f32>,
439 y: &mut CudaSlice<f32>,
440 t: usize,
441 hidden: usize,
442) -> Res<()> {
443 let streams = topology.streams;
444 let rows = topology.rows();
445 let width = streams * hidden;
446 let eps = topology.epsilon;
447 let stream = e.stream();
448
449 // FUSED PRE-CHAIN DOOR (lane/glm5-decode-diet). Engages at any t (the kernel is
450 // block-per-token, per-token bytes t-invariant like the unfused chain) whenever the
451 // stream count fits the kernel's static shared arrays; every other shape falls through
452 // to the unchanged three-kernel program below. The kernel reads the RAW mixes and
453 // applies the rowsq rescale internally, so the in-place scale write below is subsumed
454 // (nothing reads the scaled mixes after this function either way).
455 if hc_fused_pre_on() && streams <= 8 {
456 unsafe {
457 ck(
458 "hc_pre_fused",
459 k::memra_dsv4_hc_pre_fused(
460 dpf!(x, &stream),
461 dpf!(mixes, &stream),
462 dpf!(site.scale, &stream),
463 dpf!(site.base, &stream),
464 dpm!(pre_gates, &stream),
465 dpm!(post, &stream),
466 dpm!(comb, &stream),
467 dpm!(y, &stream),
468 t as i32,
469 streams as i32,
470 hidden as i32,
471 topology.sinkhorn_iterations as i32,
472 eps,
473 std::ptr::null_mut(),
474 sp(&stream),
475 ),
476 )?;
477 }
478 if HC_FUSED_PRE_DISPATCHES.fetch_add(1, std::sync::atomic::Ordering::Relaxed) == 0 {
479 eprintln!(
480 "[hc-fused-pre] engaged streams={streams} hidden={hidden} t={t} (one launch \
481 replaces rowsq_scale + sinkhorn + collapse per site; MEMRA_HC_FUSED_PRE=1)"
482 );
483 }
484 return Ok(());
485 }
486 unsafe {
487 ck(
488 "hc rowsq_scale",
489 k::memra_dsv4_rowsq_scale(
490 dpf!(x, &stream),
491 dpm!(mixes, &stream),
492 t as i32,
493 width as i32,
494 rows as i32,
495 eps,
496 sp(&stream),
497 ),
498 )?;
499 ck(
500 "hc_sinkhorn",
501 k::memra_dsv4_hc_sinkhorn_m(
502 dpf!(mixes, &stream),
503 dpf!(site.scale, &stream),
504 dpf!(site.base, &stream),
505 dpm!(pre_gates, &stream),
506 dpm!(post, &stream),
507 dpm!(comb, &stream),
508 t as i32,
509 streams as i32,
510 topology.sinkhorn_iterations as i32,
511 eps,
512 sp(&stream),
513 ),
514 )?;
515 ck(
516 "hc_collapse",
517 k::memra_dsv4_hc_collapse(
518 dpf!(x, &stream),
519 dpf!(pre_gates, &stream),
520 dpm!(y, &stream),
521 t as i32,
522 streams as i32,
523 hidden as i32,
524 sp(&stream),
525 ),
526 )?;
527 }
528 Ok(())
529}
530
531/// Persistent T=1 decode workspace for the hc glue (lane/glm5-decode-diet lever 2,
532/// `MEMRA_HC_DECODE_WS`). One per engine (pp stage), pooled on the `Engine` like
533/// `fa_part_pool`/`router_stage`: the launch-diet census measured 2,358
534/// `cuMemAllocAsync+Free` calls/token (~2.5 ms of host time feeding the sync-serialized
535/// drain cycles), and the hc glue chain — mixes, gates, comb, collapse y, the two norm
536/// scratches and the two per-site post outputs — re-allocated all of it every token. Every
537/// buffer here is FULLY OVERWRITTEN before any read on every step (GEMV beta=0, block-per-
538/// token kernels, rms_norm, hc_post), which is what makes reuse byte-identical: the same
539/// kernels read and write the same values, only the allocator calls disappear.
540///
541/// The stream-state ping-pong deliberately has ONE slot (`xb`): the walk swaps the owned
542/// in-flight state `x` with `xb` after each site's `hc_post`, so the pair rotates without a
543/// copy and the walk still returns an owned buffer to the caller (no signature churn at the
544/// stage boundary — the ppN transport consumes it exactly as before).
545pub struct HyperDecodeWs {
546 pub mixes: CudaSlice<f32>,
547 pub pre: CudaSlice<f32>,
548 pub post: CudaSlice<f32>,
549 pub comb: CudaSlice<f32>,
550 pub y: CudaSlice<f32>,
551 /// Attention-site rms_norm scratch (the walk's `h`).
552 pub h: CudaSlice<f32>,
553 /// MLP-site rms_norm scratch (the walk's `z`).
554 pub z: CudaSlice<f32>,
555 /// The `hc_post` output slot the walk ping-pongs with the in-flight stream state.
556 pub xb: CudaSlice<f32>,
557 streams: usize,
558 hidden: usize,
559}
560
561impl HyperDecodeWs {
562 pub fn new(e: &Engine, topology: &HyperTopology, hidden: usize) -> Res<Self> {
563 let streams = topology.streams;
564 Ok(Self {
565 mixes: e.uninit(topology.rows())?,
566 pre: e.uninit(streams)?,
567 post: e.uninit(streams)?,
568 comb: e.uninit(streams * streams)?,
569 y: e.uninit(hidden)?,
570 h: e.uninit(hidden)?,
571 z: e.uninit(hidden)?,
572 xb: e.uninit(streams * hidden)?,
573 streams,
574 hidden,
575 })
576 }
577
578 /// A pooled workspace is only reusable for the same trunk geometry; anything else is
579 /// rebuilt (one engine serves one loaded model in practice, this is a guard, not a path).
580 pub fn matches(&self, topology: &HyperTopology, hidden: usize) -> bool {
581 self.streams == topology.streams && self.hidden == hidden
582 }
583}
584
585/// `pre` at T=1 into the workspace: the SAME m=1 mixes program the allocating entry runs
586/// (`linear_t1_into` is `linear` at m == 1 — same cuBLASLt config, same weight pointer, same
587/// input bytes; the `pre_exact` note), then the shared `pre_finish_into` arms. Byte-identical
588/// to `pre(e, topology, site, x, 1, hidden)` with the outputs landing in `ws` instead of
589/// fresh allocations.
590pub fn pre_t1_ws(
591 e: &Engine,
592 topology: &HyperTopology,
593 site: &HyperSite,
594 x: &CudaSlice<f32>,
595 ws: &mut HyperDecodeWs,
596 hidden: usize,
597) -> Res<()> {
598 let rows = topology.rows();
599 let width = topology.streams * hidden;
600 {
601 let xr = x.slice(0..width);
602 let wv = site.fn_w.slice(0..site.fn_w.len());
603 let mut yr = ws.mixes.slice_mut(0..rows);
604 e.linear_t1_into(&xr, &wv, &mut yr, width, rows)
605 .map_err(|err| format!("hc pre_t1_ws mixes: {err}"))?;
606 }
607 let ws = &mut *ws;
608 pre_finish_into(
609 e,
610 topology,
611 site,
612 x,
613 &mut ws.mixes,
614 &mut ws.pre,
615 &mut ws.post,
616 &mut ws.comb,
617 &mut ws.y,
618 1,
619 hidden,
620 )
621}
622
623/// `post` at T=1 into the workspace's `xb` slot (the caller swaps `xb` with its in-flight
624/// state). Reads the gates `pre_t1_ws` left in `ws.post`/`ws.comb` — the same kernel, the
625/// same operand bytes as the allocating `post`.
626pub fn post_t1_ws(
627 e: &Engine,
628 topology: &HyperTopology,
629 f: &CudaSlice<f32>,
630 residual: &CudaSlice<f32>,
631 ws: &mut HyperDecodeWs,
632 hidden: usize,
633) -> Res<()> {
634 let stream = e.stream();
635 let ws = &mut *ws;
636 unsafe {
637 ck(
638 "hc_post",
639 k::memra_dsv4_hc_post(
640 dpf!(f, &stream),
641 dpf!(residual, &stream),
642 dpf!(ws.post, &stream),
643 dpf!(ws.comb, &stream),
644 dpm!(ws.xb, &stream),
645 1,
646 topology.streams as i32,
647 hidden as i32,
648 sp(&stream),
649 ),
650 )?;
651 }
652 Ok(())
653}
654
655/// One site's post-branch half (`hc_post`): `out[t, k, :] = post[t, k]·f[t, :] + Σ_j
656/// comb[t, j, k]·residual[t, j, :]`. `residual` is the site's INPUT stream state, not the
657/// layer's — the MLP site's residual is the attention site's output.
658pub fn post(
659 e: &Engine,
660 topology: &HyperTopology,
661 f: &CudaSlice<f32>,
662 residual: &CudaSlice<f32>,
663 mix: &HcMix,
664 t: usize,
665 hidden: usize,
666) -> Res<CudaSlice<f32>> {
667 let streams = topology.streams;
668 let mut out = e.uninit(t * streams * hidden)?;
669 let stream = e.stream();
670 unsafe {
671 ck(
672 "hc_post",
673 k::memra_dsv4_hc_post(
674 dpf!(f, &stream),
675 dpf!(residual, &stream),
676 dpf!(mix.post, &stream),
677 dpf!(mix.comb, &stream),
678 dpm!(out, &stream),
679 t as i32,
680 streams as i32,
681 hidden as i32,
682 sp(&stream),
683 ),
684 )?;
685 }
686 Ok(out)
687}
688
689/// UNWEIGHTED stream-mean contraction `[tokens, streams, hidden]` -> `[tokens, hidden]` —
690/// the `hc_contract` the glm5 DFlash2 drafter's aux-hidden features are defined by (the
691/// probe's capture seam: mean over the hc_mult stream blocks of the completed layer output,
692/// == the SGLang glm5_next integration's pinned definition). Deliberately NOT keyed on
693/// `topology.collapse`: the drafter contract is the mean by definition, whatever the trunk
694/// exit does (for glm5_next the exit IS `Mean`, so this is also the collapse kernel).
695pub fn contract_mean(
696 e: &Engine,
697 topology: &HyperTopology,
698 x: &CudaSlice<f32>,
699 t: usize,
700 hidden: usize,
701) -> Res<CudaSlice<f32>> {
702 let streams = topology.streams;
703 let stream = e.stream();
704 let mut out = e.uninit(t * hidden)?;
705 unsafe {
706 ck(
707 "hc_mean",
708 k::memra_dsv4_hc_mean(
709 dpf!(x, &stream),
710 dpm!(out, &stream),
711 t as i32,
712 streams as i32,
713 hidden as i32,
714 sp(&stream),
715 ),
716 )?;
717 }
718 Ok(out)
719}
720
721/// Trunk exit: `[tokens, streams, hidden]` -> `[tokens, hidden]`, keyed on the plan's collapse.
722/// `Mean` is glm5_next's unweighted mean (`Glm5NextTextHyperHead`); `GatedHead` is dsv4's
723/// sigmoid-gated pre-only collapse (`dsv4_forward::hc_head`) and needs the head trio.
724pub fn collapse(
725 e: &Engine,
726 topology: &HyperTopology,
727 head: Option<&HyperHead>,
728 x: &CudaSlice<f32>,
729 t: usize,
730 hidden: usize,
731) -> Res<CudaSlice<f32>> {
732 let streams = topology.streams;
733 let stream = e.stream();
734 let mut out = e.uninit(t * hidden)?;
735 match topology.collapse {
736 HcCollapse::Mean => unsafe {
737 ck(
738 "hc_mean",
739 k::memra_dsv4_hc_mean(
740 dpf!(x, &stream),
741 dpm!(out, &stream),
742 t as i32,
743 streams as i32,
744 hidden as i32,
745 sp(&stream),
746 ),
747 )?;
748 },
749 HcCollapse::GatedHead => {
750 let head = head.ok_or_else(|| {
751 "HcCollapse::GatedHead reached the trunk exit with no head trio loaded".to_string()
752 })?;
753 let width = streams * hidden;
754 let mut mixes = e.linear(x, &head.fn_w, t, width, streams)?;
755 let mut gates = e.uninit(t * streams)?;
756 unsafe {
757 ck(
758 "hc_head rowsq_scale",
759 k::memra_dsv4_rowsq_scale(
760 dpf!(x, &stream),
761 dpm!(mixes, &stream),
762 t as i32,
763 width as i32,
764 streams as i32,
765 topology.epsilon,
766 sp(&stream),
767 ),
768 )?;
769 ck(
770 "hc_head_pre",
771 k::memra_dsv4_hc_head_pre_m(
772 dpf!(mixes, &stream),
773 dpf!(head.scale, &stream),
774 dpf!(head.base, &stream),
775 dpm!(gates, &stream),
776 t as i32,
777 streams as i32,
778 topology.epsilon,
779 sp(&stream),
780 ),
781 )?;
782 ck(
783 "hc_head collapse",
784 k::memra_dsv4_hc_collapse(
785 dpf!(x, &stream),
786 dpf!(gates, &stream),
787 dpm!(out, &stream),
788 t as i32,
789 streams as i32,
790 hidden as i32,
791 sp(&stream),
792 ),
793 )?;
794 }
795 }
796 }
797 Ok(out)
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803 use memra_gguf::model_plan::{
804 ActivationPlan, AttentionPlan, DenseMlpPlan, DraftSourcePlan, KimiDeltaNetPlan, LayerPlan,
805 MlpPlan, NormKind, NormPlan, StatePlan, WeightTransform,
806 };
807
808 fn norm() -> NormPlan {
809 NormPlan {
810 kind: NormKind::Rms,
811 epsilon: 1e-5,
812 weight_transform: WeightTransform::Identity,
813 }
814 }
815
816 fn layer(index: u32, residual: ResidualTopology) -> LayerPlan {
817 LayerPlan {
818 index,
819 pre_attention_norm: norm(),
820 attention: AttentionPlan::KimiDeltaNet(KimiDeltaNetPlan {
821 num_heads: 1,
822 head_dim: 128,
823 conv_kernel: 4,
824 gate_lower_bound: -5.0,
825 }),
826 pre_mlp_norm: norm(),
827 mlp: MlpPlan::Dense(DenseMlpPlan {
828 intermediate_size: 16,
829 activation: ActivationPlan::Silu,
830 }),
831 residual,
832 state: StatePlan::Recurrent {
833 conv_width: 384,
834 conv_kernel: 4,
835 state_width: 16384,
836 },
837 ple: None,
838 sparse_overlay: None,
839 }
840 }
841
842 fn plan(residuals: [ResidualTopology; 2]) -> ModelPlan {
843 ModelPlan {
844 arch: memra_gguf::config::Arch::Glm5Next,
845 hidden_size: 8,
846 vocab_size: 16,
847 context_length: 32,
848 embedding_scale: 1.0,
849 vision: None,
850 multimodal: None,
851 layers: vec![layer(0, residuals[0]), layer(1, residuals[1])],
852 output_norm: norm(),
853 logits: Vec::new(),
854 mtp_blocks: Vec::new(),
855 drafter: None,
856 exit_mixer: None,
857 draft_source: DraftSourcePlan::Embedded,
858 sampling_defaults: None,
859 partition_boundaries: Vec::new(),
860 }
861 }
862
863 fn hc(streams: u32) -> ResidualTopology {
864 ResidualTopology::HyperConnections {
865 streams,
866 epsilon: 1e-6,
867 sinkhorn_iterations: 20,
868 collapse: HcCollapse::Mean,
869 }
870 }
871
872 #[test]
873 fn serial_trunk_has_no_topology() {
874 let plan = plan([ResidualTopology::Serial, ResidualTopology::Serial]);
875 assert!(HyperTopology::from_plan(&plan).unwrap().is_none());
876 }
877
878 #[test]
879 fn uniform_trunk_yields_the_plans_constants() {
880 let plan = plan([hc(4), hc(4)]);
881 let topology = HyperTopology::from_plan(&plan).unwrap().unwrap();
882 assert_eq!(topology.streams, 4);
883 assert_eq!(topology.sinkhorn_iterations, 20);
884 assert_eq!(topology.collapse, HcCollapse::Mean);
885 // pre gates + post gates + the streams x streams combination block.
886 assert_eq!(topology.rows(), 24);
887 }
888
889 #[test]
890 fn a_mixed_trunk_is_refused_in_both_orders() {
891 for residuals in [
892 [hc(4), ResidualTopology::Serial],
893 [ResidualTopology::Serial, hc(4)],
894 [hc(4), hc(2)],
895 ] {
896 assert!(
897 HyperTopology::from_plan(&plan(residuals)).is_err(),
898 "a non-uniform trunk must be refused, not silently keyed off layer 0"
899 );
900 }
901 }
902
903 #[test]
904 fn zero_iterations_are_refused() {
905 let bad = ResidualTopology::HyperConnections {
906 streams: 4,
907 epsilon: 1e-6,
908 sinkhorn_iterations: 0,
909 collapse: HcCollapse::Mean,
910 };
911 assert!(HyperTopology::from_plan(&plan([bad, bad])).is_err());
912 }
913}