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'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}