Skip to main content

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}