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's `=1` arm: incremented at the arm's own
306/// call site, announced once per boot — the spec-engagement receipt the gate and any box A/B
307/// arm must show ([bf16-mmv] RESIDENT lesson: engagement lines are receipts, never inferred).
308pub static HC_FUSED_PRE_DISPATCHES: std::sync::atomic::AtomicU64 =
309 std::sync::atomic::AtomicU64::new(0);
310
311/// Engagement counter for the fused pre-chain door's `=2` arm (lane/b200-sinkhorn-fusion-
312/// 20260902 follow-up), same discipline as `HC_FUSED_PRE_DISPATCHES`.
313pub static HC_FUSED_PRE_V2_DISPATCHES: std::sync::atomic::AtomicU64 =
314 std::sync::atomic::AtomicU64::new(0);
315
316/// The three states of `MEMRA_HC_FUSED_PRE` (default OFF): the unfused three-kernel chain,
317/// the `=1` fused kernel (`memra_dsv4_hc_pre_fused`, lane/glm5-decode-diet 2026-08-31), or
318/// the `=2` fused kernel (`memra_dsv4_hc_pre_fused_v2`, lane/b200-sinkhorn-fusion-20260902 —
319/// same stages, warp-scoped Sinkhorn sync). Any other value (unset, `0`, or unrecognized)
320/// stays `Off`, the existing "read per call" rollback-seam contract.
321#[derive(Clone, Copy, PartialEq, Eq)]
322enum HcFusedPreArm {
323 Off,
324 V1,
325 V2,
326}
327
328/// `MEMRA_HC_FUSED_PRE` (default OFF, both `1` and `2` opt in): the three-kernel site
329/// pre-chain (rowsq_scale + Sinkhorn + collapse) runs as ONE launch per site — bit-identical
330/// to the unfused chain by construction in both arms (verbatim bodies, asserted bytewise in
331/// `hc_fused_pre_gpu.rs` for `=1` and by `hc-fused-gate` for `=1` vs `=2`). Read PER CALL
332/// (the `MEMRA_MOE_FUSED_EPI` rollback-seam precedent), so arms can alternate inside one
333/// process and the flag is a live rollback seam.
334fn hc_fused_pre_arm() -> HcFusedPreArm {
335 match std::env::var("MEMRA_HC_FUSED_PRE").as_deref() {
336 Ok("1") => HcFusedPreArm::V1,
337 Ok("2") => HcFusedPreArm::V2,
338 _ => HcFusedPreArm::Off,
339 }
340}
341
342/// Model entry (`hc_expand`): `[tokens, hidden]` embeddings -> `[tokens, streams, hidden]`.
343pub fn expand(
344 e: &Engine,
345 topology: &HyperTopology,
346 embedded: &CudaSlice<f32>,
347 t: usize,
348 hidden: usize,
349) -> Res<CudaSlice<f32>> {
350 let streams = topology.streams;
351 let mut out = e.uninit(t * streams * hidden)?;
352 let stream = e.stream();
353 unsafe {
354 ck(
355 "hc_expand",
356 k::memra_dsv4_hc_expand(
357 dpf!(embedded, &stream),
358 dpm!(out, &stream),
359 t as i32,
360 streams as i32,
361 hidden as i32,
362 sp(&stream),
363 ),
364 )?;
365 }
366 Ok(out)
367}
368
369/// One site's pre-branch half (`hc_pre`): mixes GEMM, per-token RMS rescale, Sinkhorn, stream
370/// collapse. Returns the branch input `[tokens, hidden]` and the gates its `post` half needs.
371pub fn pre(
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 width = topology.streams * hidden;
380 let mixes = e.linear(x, &site.fn_w, t, width, topology.rows())?;
381 pre_finish(e, topology, site, x, mixes, t, hidden)
382}
383
384/// `pre` with the DECODE-EXACT mixing GEMM: each token's mix coefficients come from the
385/// SAME m=1 cuBLASLt program the serial T=1 decode step runs (`linear_t1_into` is `linear`
386/// at m == 1 on a row view — same config, same weight pointer, same input bytes), instead
387/// of one m=t call whose n-dependent reduction split changes every output bit (the lt_ndep
388/// probe documented on `Engine::linear_decode_exact`). Everything after the GEMM is the
389/// per-token kernel set `pre` already runs — block-per-token programs whose per-token bytes
390/// do not depend on t. This is the entry the BATCHED hyper decode walk uses so that row b
391/// of a B-row tick is bit-identical to that session's solo `decode_step_hyper` step.
392pub fn pre_exact(
393 e: &Engine,
394 topology: &HyperTopology,
395 site: &HyperSite,
396 x: &CudaSlice<f32>,
397 t: usize,
398 hidden: usize,
399) -> Res<(CudaSlice<f32>, HcMix)> {
400 let rows = topology.rows();
401 let width = topology.streams * hidden;
402 let mut mixes = e.uninit(t * rows)?;
403 for r in 0..t {
404 let xr = x.slice(r * width..(r + 1) * width);
405 let wv = site.fn_w.slice(0..site.fn_w.len());
406 let mut yr = mixes.slice_mut(r * rows..(r + 1) * rows);
407 e.linear_t1_into(&xr, &wv, &mut yr, width, rows)
408 .map_err(|err| format!("hc pre_exact row {r}: {err}"))?;
409 }
410 pre_finish(e, topology, site, x, mixes, t, hidden)
411}
412
413/// The per-token half `pre` and `pre_exact` share: RMS rescale of the mix coefficients,
414/// Sinkhorn, stream collapse. Every kernel here is a block-per-token program (grid over t),
415/// so per-token output bytes are invariant to t — the two entries differ ONLY in how the
416/// mixes GEMM reduces.
417fn pre_finish(
418 e: &Engine,
419 topology: &HyperTopology,
420 site: &HyperSite,
421 x: &CudaSlice<f32>,
422 mut mixes: CudaSlice<f32>,
423 t: usize,
424 hidden: usize,
425) -> Res<(CudaSlice<f32>, HcMix)> {
426 let streams = topology.streams;
427 let mut pre_gates = e.uninit(t * streams)?;
428 let mut post = e.uninit(t * streams)?;
429 let mut comb = e.uninit(t * streams * streams)?;
430 let mut y = e.uninit(t * hidden)?;
431 pre_finish_into(
432 e,
433 topology,
434 site,
435 x,
436 &mut mixes,
437 &mut pre_gates,
438 &mut post,
439 &mut comb,
440 &mut y,
441 t,
442 hidden,
443 )?;
444 Ok((y, HcMix { post, comb }))
445}
446
447/// `pre_finish`'s kernel arms on caller-owned outputs — shared by the allocating entry above
448/// and the persistent-workspace decode walk (`pre_t1_ws`), so the two cannot drift. Both arms
449/// fully overwrite every output element, which is what makes workspace reuse byte-identical.
450#[allow(clippy::too_many_arguments)] // allow: the parameter list mirrors the kernel/FFI contract; the workspace caller passes disjoint field borrows
451fn pre_finish_into(
452 e: &Engine,
453 topology: &HyperTopology,
454 site: &HyperSite,
455 x: &CudaSlice<f32>,
456 mixes: &mut CudaSlice<f32>,
457 pre_gates: &mut CudaSlice<f32>,
458 post: &mut CudaSlice<f32>,
459 comb: &mut CudaSlice<f32>,
460 y: &mut CudaSlice<f32>,
461 t: usize,
462 hidden: usize,
463) -> Res<()> {
464 let streams = topology.streams;
465 let rows = topology.rows();
466 let width = streams * hidden;
467 let eps = topology.epsilon;
468 let stream = e.stream();
469
470 // FUSED PRE-CHAIN DOOR (lane/glm5-decode-diet; `=2` arm lane/b200-sinkhorn-fusion-
471 // 20260902). Engages at any t (block-per-token, per-token bytes t-invariant like the
472 // unfused chain) whenever the stream count fits the kernel's static shared arrays;
473 // every other shape falls through to the unchanged three-kernel program below. Both
474 // kernels read the RAW mixes and apply the rowsq rescale internally, so the in-place
475 // scale write below is subsumed (nothing reads the scaled mixes after this function
476 // either way).
477 let fused_arm = hc_fused_pre_arm();
478 if fused_arm != HcFusedPreArm::Off && streams <= 8 {
479 let (label, rc) = unsafe {
480 match fused_arm {
481 HcFusedPreArm::V1 => (
482 "hc_pre_fused",
483 k::memra_dsv4_hc_pre_fused(
484 dpf!(x, &stream),
485 dpf!(mixes, &stream),
486 dpf!(site.scale, &stream),
487 dpf!(site.base, &stream),
488 dpm!(pre_gates, &stream),
489 dpm!(post, &stream),
490 dpm!(comb, &stream),
491 dpm!(y, &stream),
492 t as i32,
493 streams as i32,
494 hidden as i32,
495 topology.sinkhorn_iterations as i32,
496 eps,
497 std::ptr::null_mut(),
498 sp(&stream),
499 ),
500 ),
501 HcFusedPreArm::V2 => (
502 "hc_pre_fused_v2",
503 k::memra_dsv4_hc_pre_fused_v2(
504 dpf!(x, &stream),
505 dpf!(mixes, &stream),
506 dpf!(site.scale, &stream),
507 dpf!(site.base, &stream),
508 dpm!(pre_gates, &stream),
509 dpm!(post, &stream),
510 dpm!(comb, &stream),
511 dpm!(y, &stream),
512 t as i32,
513 streams as i32,
514 hidden as i32,
515 topology.sinkhorn_iterations as i32,
516 eps,
517 std::ptr::null_mut(),
518 sp(&stream),
519 ),
520 ),
521 HcFusedPreArm::Off => unreachable!("guarded by the enclosing if"),
522 }
523 };
524 ck(label, rc)?;
525 let (counter, tag) = match fused_arm {
526 HcFusedPreArm::V1 => (&HC_FUSED_PRE_DISPATCHES, "1"),
527 HcFusedPreArm::V2 => (&HC_FUSED_PRE_V2_DISPATCHES, "2"),
528 HcFusedPreArm::Off => unreachable!("guarded by the enclosing if"),
529 };
530 if counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed) == 0 {
531 eprintln!(
532 "[hc-fused-pre] engaged streams={streams} hidden={hidden} t={t} arm={tag} \
533 (one launch replaces rowsq_scale + sinkhorn + collapse per site; \
534 MEMRA_HC_FUSED_PRE={tag})"
535 );
536 }
537 return Ok(());
538 }
539 unsafe {
540 ck(
541 "hc rowsq_scale",
542 k::memra_dsv4_rowsq_scale(
543 dpf!(x, &stream),
544 dpm!(mixes, &stream),
545 t as i32,
546 width as i32,
547 rows as i32,
548 eps,
549 sp(&stream),
550 ),
551 )?;
552 ck(
553 "hc_sinkhorn",
554 k::memra_dsv4_hc_sinkhorn_m(
555 dpf!(mixes, &stream),
556 dpf!(site.scale, &stream),
557 dpf!(site.base, &stream),
558 dpm!(pre_gates, &stream),
559 dpm!(post, &stream),
560 dpm!(comb, &stream),
561 t as i32,
562 streams as i32,
563 topology.sinkhorn_iterations as i32,
564 eps,
565 sp(&stream),
566 ),
567 )?;
568 ck(
569 "hc_collapse",
570 k::memra_dsv4_hc_collapse(
571 dpf!(x, &stream),
572 dpf!(pre_gates, &stream),
573 dpm!(y, &stream),
574 t as i32,
575 streams as i32,
576 hidden as i32,
577 sp(&stream),
578 ),
579 )?;
580 }
581 Ok(())
582}
583
584/// Persistent T=1 decode workspace for the hc glue (lane/glm5-decode-diet lever 2,
585/// `MEMRA_HC_DECODE_WS`). One per engine (pp stage), pooled on the `Engine` like
586/// `fa_part_pool`/`router_stage`: the launch-diet census measured 2,358
587/// `cuMemAllocAsync+Free` calls/token (~2.5 ms of host time feeding the sync-serialized
588/// drain cycles), and the hc glue chain — mixes, gates, comb, collapse y, the two norm
589/// scratches and the two per-site post outputs — re-allocated all of it every token. Every
590/// buffer here is FULLY OVERWRITTEN before any read on every step (GEMV beta=0, block-per-
591/// token kernels, rms_norm, hc_post), which is what makes reuse byte-identical: the same
592/// kernels read and write the same values, only the allocator calls disappear.
593///
594/// The stream-state ping-pong deliberately has ONE slot (`xb`): the walk swaps the owned
595/// in-flight state `x` with `xb` after each site's `hc_post`, so the pair rotates without a
596/// copy and the walk still returns an owned buffer to the caller (no signature churn at the
597/// stage boundary — the ppN transport consumes it exactly as before).
598pub struct HyperDecodeWs {
599 pub mixes: CudaSlice<f32>,
600 pub pre: CudaSlice<f32>,
601 pub post: CudaSlice<f32>,
602 pub comb: CudaSlice<f32>,
603 pub y: CudaSlice<f32>,
604 /// Attention-site rms_norm scratch (the walk's `h`).
605 pub h: CudaSlice<f32>,
606 /// MLP-site rms_norm scratch (the walk's `z`).
607 pub z: CudaSlice<f32>,
608 /// The `hc_post` output slot the walk ping-pongs with the in-flight stream state.
609 pub xb: CudaSlice<f32>,
610 streams: usize,
611 hidden: usize,
612}
613
614impl HyperDecodeWs {
615 pub fn new(e: &Engine, topology: &HyperTopology, hidden: usize) -> Res<Self> {
616 let streams = topology.streams;
617 Ok(Self {
618 mixes: e.uninit(topology.rows())?,
619 pre: e.uninit(streams)?,
620 post: e.uninit(streams)?,
621 comb: e.uninit(streams * streams)?,
622 y: e.uninit(hidden)?,
623 h: e.uninit(hidden)?,
624 z: e.uninit(hidden)?,
625 xb: e.uninit(streams * hidden)?,
626 streams,
627 hidden,
628 })
629 }
630
631 /// A pooled workspace is only reusable for the same trunk geometry; anything else is
632 /// rebuilt (one engine serves one loaded model in practice, this is a guard, not a path).
633 pub fn matches(&self, topology: &HyperTopology, hidden: usize) -> bool {
634 self.streams == topology.streams && self.hidden == hidden
635 }
636}
637
638/// `pre` at T=1 into the workspace: the SAME m=1 mixes program the allocating entry runs
639/// (`linear_t1_into` is `linear` at m == 1 — same cuBLASLt config, same weight pointer, same
640/// input bytes; the `pre_exact` note), then the shared `pre_finish_into` arms. Byte-identical
641/// to `pre(e, topology, site, x, 1, hidden)` with the outputs landing in `ws` instead of
642/// fresh allocations.
643pub fn pre_t1_ws(
644 e: &Engine,
645 topology: &HyperTopology,
646 site: &HyperSite,
647 x: &CudaSlice<f32>,
648 ws: &mut HyperDecodeWs,
649 hidden: usize,
650) -> Res<()> {
651 let rows = topology.rows();
652 let width = topology.streams * hidden;
653 {
654 let xr = x.slice(0..width);
655 let wv = site.fn_w.slice(0..site.fn_w.len());
656 let mut yr = ws.mixes.slice_mut(0..rows);
657 e.linear_t1_into(&xr, &wv, &mut yr, width, rows)
658 .map_err(|err| format!("hc pre_t1_ws mixes: {err}"))?;
659 }
660 let ws = &mut *ws;
661 pre_finish_into(
662 e,
663 topology,
664 site,
665 x,
666 &mut ws.mixes,
667 &mut ws.pre,
668 &mut ws.post,
669 &mut ws.comb,
670 &mut ws.y,
671 1,
672 hidden,
673 )
674}
675
676/// `post` at T=1 into the workspace's `xb` slot (the caller swaps `xb` with its in-flight
677/// state). Reads the gates `pre_t1_ws` left in `ws.post`/`ws.comb` — the same kernel, the
678/// same operand bytes as the allocating `post`.
679pub fn post_t1_ws(
680 e: &Engine,
681 topology: &HyperTopology,
682 f: &CudaSlice<f32>,
683 residual: &CudaSlice<f32>,
684 ws: &mut HyperDecodeWs,
685 hidden: usize,
686) -> Res<()> {
687 let stream = e.stream();
688 let ws = &mut *ws;
689 unsafe {
690 ck(
691 "hc_post",
692 k::memra_dsv4_hc_post(
693 dpf!(f, &stream),
694 dpf!(residual, &stream),
695 dpf!(ws.post, &stream),
696 dpf!(ws.comb, &stream),
697 dpm!(ws.xb, &stream),
698 1,
699 topology.streams as i32,
700 hidden as i32,
701 sp(&stream),
702 ),
703 )?;
704 }
705 Ok(())
706}
707
708/// One site's post-branch half (`hc_post`): `out[t, k, :] = post[t, k]·f[t, :] + Σ_j
709/// comb[t, j, k]·residual[t, j, :]`. `residual` is the site's INPUT stream state, not the
710/// layer's — the MLP site's residual is the attention site's output.
711pub fn post(
712 e: &Engine,
713 topology: &HyperTopology,
714 f: &CudaSlice<f32>,
715 residual: &CudaSlice<f32>,
716 mix: &HcMix,
717 t: usize,
718 hidden: usize,
719) -> Res<CudaSlice<f32>> {
720 let streams = topology.streams;
721 let mut out = e.uninit(t * streams * hidden)?;
722 let stream = e.stream();
723 unsafe {
724 ck(
725 "hc_post",
726 k::memra_dsv4_hc_post(
727 dpf!(f, &stream),
728 dpf!(residual, &stream),
729 dpf!(mix.post, &stream),
730 dpf!(mix.comb, &stream),
731 dpm!(out, &stream),
732 t as i32,
733 streams as i32,
734 hidden as i32,
735 sp(&stream),
736 ),
737 )?;
738 }
739 Ok(out)
740}
741
742/// UNWEIGHTED stream-mean contraction `[tokens, streams, hidden]` -> `[tokens, hidden]` —
743/// the `hc_contract` the glm5 DFlash2 drafter's aux-hidden features are defined by (the
744/// probe's capture seam: mean over the hc_mult stream blocks of the completed layer output,
745/// == the SGLang glm5_next integration's pinned definition). Deliberately NOT keyed on
746/// `topology.collapse`: the drafter contract is the mean by definition, whatever the trunk
747/// exit does (for glm5_next the exit IS `Mean`, so this is also the collapse kernel).
748pub fn contract_mean(
749 e: &Engine,
750 topology: &HyperTopology,
751 x: &CudaSlice<f32>,
752 t: usize,
753 hidden: usize,
754) -> Res<CudaSlice<f32>> {
755 let streams = topology.streams;
756 let stream = e.stream();
757 let mut out = e.uninit(t * hidden)?;
758 unsafe {
759 ck(
760 "hc_mean",
761 k::memra_dsv4_hc_mean(
762 dpf!(x, &stream),
763 dpm!(out, &stream),
764 t as i32,
765 streams as i32,
766 hidden as i32,
767 sp(&stream),
768 ),
769 )?;
770 }
771 Ok(out)
772}
773
774/// Trunk exit: `[tokens, streams, hidden]` -> `[tokens, hidden]`, keyed on the plan's collapse.
775/// `Mean` is glm5_next's unweighted mean (`Glm5NextTextHyperHead`); `GatedHead` is dsv4's
776/// sigmoid-gated pre-only collapse (`dsv4_forward::hc_head`) and needs the head trio.
777pub fn collapse(
778 e: &Engine,
779 topology: &HyperTopology,
780 head: Option<&HyperHead>,
781 x: &CudaSlice<f32>,
782 t: usize,
783 hidden: usize,
784) -> Res<CudaSlice<f32>> {
785 let streams = topology.streams;
786 let stream = e.stream();
787 let mut out = e.uninit(t * hidden)?;
788 match topology.collapse {
789 HcCollapse::Mean => unsafe {
790 ck(
791 "hc_mean",
792 k::memra_dsv4_hc_mean(
793 dpf!(x, &stream),
794 dpm!(out, &stream),
795 t as i32,
796 streams as i32,
797 hidden as i32,
798 sp(&stream),
799 ),
800 )?;
801 },
802 HcCollapse::GatedHead => {
803 let head = head.ok_or_else(|| {
804 "HcCollapse::GatedHead reached the trunk exit with no head trio loaded".to_string()
805 })?;
806 let width = streams * hidden;
807 let mut mixes = e.linear(x, &head.fn_w, t, width, streams)?;
808 let mut gates = e.uninit(t * streams)?;
809 unsafe {
810 ck(
811 "hc_head rowsq_scale",
812 k::memra_dsv4_rowsq_scale(
813 dpf!(x, &stream),
814 dpm!(mixes, &stream),
815 t as i32,
816 width as i32,
817 streams as i32,
818 topology.epsilon,
819 sp(&stream),
820 ),
821 )?;
822 ck(
823 "hc_head_pre",
824 k::memra_dsv4_hc_head_pre_m(
825 dpf!(mixes, &stream),
826 dpf!(head.scale, &stream),
827 dpf!(head.base, &stream),
828 dpm!(gates, &stream),
829 t as i32,
830 streams as i32,
831 topology.epsilon,
832 sp(&stream),
833 ),
834 )?;
835 ck(
836 "hc_head collapse",
837 k::memra_dsv4_hc_collapse(
838 dpf!(x, &stream),
839 dpf!(gates, &stream),
840 dpm!(out, &stream),
841 t as i32,
842 streams as i32,
843 hidden as i32,
844 sp(&stream),
845 ),
846 )?;
847 }
848 }
849 }
850 Ok(out)
851}
852
853#[cfg(test)]
854mod tests {
855 use super::*;
856 use memra_gguf::model_plan::{
857 ActivationPlan, AttentionPlan, DenseMlpPlan, DraftSourcePlan, KimiDeltaNetPlan, LayerPlan,
858 MlpPlan, NormKind, NormPlan, StatePlan, WeightTransform,
859 };
860
861 fn norm() -> NormPlan {
862 NormPlan {
863 kind: NormKind::Rms,
864 epsilon: 1e-5,
865 weight_transform: WeightTransform::Identity,
866 }
867 }
868
869 fn layer(index: u32, residual: ResidualTopology) -> LayerPlan {
870 LayerPlan {
871 index,
872 pre_attention_norm: norm(),
873 attention: AttentionPlan::KimiDeltaNet(KimiDeltaNetPlan {
874 num_heads: 1,
875 head_dim: 128,
876 conv_kernel: 4,
877 gate_lower_bound: -5.0,
878 }),
879 pre_mlp_norm: norm(),
880 mlp: MlpPlan::Dense(DenseMlpPlan {
881 intermediate_size: 16,
882 activation: ActivationPlan::Silu,
883 }),
884 residual,
885 state: StatePlan::Recurrent {
886 conv_width: 384,
887 conv_kernel: 4,
888 state_width: 16384,
889 },
890 ple: None,
891 sparse_overlay: None,
892 }
893 }
894
895 fn plan(residuals: [ResidualTopology; 2]) -> ModelPlan {
896 ModelPlan {
897 arch: memra_gguf::config::Arch::Glm5Next,
898 hidden_size: 8,
899 vocab_size: 16,
900 context_length: 32,
901 embedding_scale: 1.0,
902 vision: None,
903 multimodal: None,
904 layers: vec![layer(0, residuals[0]), layer(1, residuals[1])],
905 output_norm: norm(),
906 logits: Vec::new(),
907 mtp_blocks: Vec::new(),
908 drafter: None,
909 exit_mixer: None,
910 draft_source: DraftSourcePlan::Embedded,
911 sampling_defaults: None,
912 partition_boundaries: Vec::new(),
913 }
914 }
915
916 fn hc(streams: u32) -> ResidualTopology {
917 ResidualTopology::HyperConnections {
918 streams,
919 epsilon: 1e-6,
920 sinkhorn_iterations: 20,
921 collapse: HcCollapse::Mean,
922 }
923 }
924
925 #[test]
926 fn serial_trunk_has_no_topology() {
927 let plan = plan([ResidualTopology::Serial, ResidualTopology::Serial]);
928 assert!(HyperTopology::from_plan(&plan).unwrap().is_none());
929 }
930
931 #[test]
932 fn uniform_trunk_yields_the_plans_constants() {
933 let plan = plan([hc(4), hc(4)]);
934 let topology = HyperTopology::from_plan(&plan).unwrap().unwrap();
935 assert_eq!(topology.streams, 4);
936 assert_eq!(topology.sinkhorn_iterations, 20);
937 assert_eq!(topology.collapse, HcCollapse::Mean);
938 // pre gates + post gates + the streams x streams combination block.
939 assert_eq!(topology.rows(), 24);
940 }
941
942 #[test]
943 fn a_mixed_trunk_is_refused_in_both_orders() {
944 for residuals in [
945 [hc(4), ResidualTopology::Serial],
946 [ResidualTopology::Serial, hc(4)],
947 [hc(4), hc(2)],
948 ] {
949 assert!(
950 HyperTopology::from_plan(&plan(residuals)).is_err(),
951 "a non-uniform trunk must be refused, not silently keyed off layer 0"
952 );
953 }
954 }
955
956 #[test]
957 fn zero_iterations_are_refused() {
958 let bad = ResidualTopology::HyperConnections {
959 streams: 4,
960 epsilon: 1e-6,
961 sinkhorn_iterations: 0,
962 collapse: HcCollapse::Mean,
963 };
964 assert!(HyperTopology::from_plan(&plan([bad, bad])).is_err());
965 }
966}