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