spark_model/layers/ops/hyper_connection_dispatch.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! One call site per mHC entry point, for both variants.
4//!
5//! Atlas now runs two different hyper-connection families over the same
6//! `[T, hc_mult, H]` FP32 highway:
7//!
8//! * DeepSeek-V4's — a Sinkhorn-normalized mix over `hc_fn`/`hc_scale`/
9//! `hc_base`, emitting a `[hc, hc]` combine matrix that `hc_post` mixes the
10//! saved streams through.
11//! * Qwen3.8-Flash-Next's — a low-rank pair (rank 320) with a grouped
12//! RMSNorm, emitting one scalar per stream that `hc_post` scales by.
13//!
14//! They share the four kernel NAMES (a model shadow overrides the whole
15//! `hyper_connection.cu` file) but take different argument lists. Rather than
16//! branch at each of the 23 call sites across `prefill_inner`, `decode_inner`
17//! and `multi_seq`, the branch lives here once, behind a signature that is
18//! the union of both.
19//!
20//! **`post_out` doubles as the low-rank injection vector.** Both are `[T, hc]`
21//! f32 and both are consumed by the matching `hc_post`, so the existing
22//! `ctx.buffers.hc_post()` serves each variant without a second allocation.
23//! `comb_out` is untouched on the low-rank path — Qwen scales each stream by
24//! one scalar where DeepSeek mixes them through a full matrix.
25//!
26//! Selection is by WEIGHTS (`lowrank.is_some()`), never by model name. A site
27//! carrying both would be a load-time bug rather than a silent coin-flip.
28
29use anyhow::Result;
30use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
31
32use super::hyper_connection as sinkhorn;
33use super::hyper_connection_lowrank as lowrank;
34use crate::layers::qwen3_attention::{HcHeadWeights, HcSiteWeights, HcWeights};
35
36/// Which family a site's weights select.
37///
38/// Exposed so a caller can answer questions the launch itself cannot — most
39/// importantly whether to apply the layer's `input_norm` after `hc_pre`.
40/// Qwen has no per-layer `input_layernorm`; `hc_norm` inside `hc_pre` plays
41/// that role, and the loader's ones-filled placeholder does NOT make a second
42/// RMS pass an identity.
43#[derive(Clone, Copy, PartialEq, Eq, Debug)]
44pub enum HcVariant {
45 /// DeepSeek-V4: Sinkhorn mix, `[hc, hc]` combine matrix.
46 Sinkhorn,
47 /// Qwen3.8-Flash-Next: low-rank pair, per-stream scalar injection.
48 LowRank,
49}
50
51impl HcVariant {
52 pub fn of_site(site: &HcSiteWeights) -> Self {
53 if site.lowrank.is_some() {
54 Self::LowRank
55 } else {
56 Self::Sinkhorn
57 }
58 }
59
60 pub fn of(hc: &HcWeights) -> Self {
61 Self::of_site(&hc.attn)
62 }
63
64 /// Whether the block's own `input_norm` should run on `hc_pre`'s output.
65 /// False for Qwen — see the type's doc comment.
66 pub fn applies_block_input_norm(self) -> bool {
67 self == Self::Sinkhorn
68 }
69}
70
71/// Collapse the streams to one and emit whatever the matching `hc_post`
72/// needs — `post`+`comb` for Sinkhorn, the injection vector in `post_out` for
73/// low-rank.
74#[allow(clippy::too_many_arguments)]
75pub fn hc_pre_site(
76 gpu: &dyn GpuBackend,
77 kernel: KernelHandle,
78 streams: DevicePtr,
79 site: &HcSiteWeights,
80 hc: &HcWeights,
81 y_out: DevicePtr,
82 post_out: DevicePtr,
83 comb_out: DevicePtr,
84 scratch: DevicePtr,
85 num_tokens: u32,
86 hidden_size: u32,
87 norm_eps: f32,
88 stream: u64,
89) -> Result<()> {
90 match &site.lowrank {
91 Some(w) => lowrank::hc_pre_lowrank(
92 gpu,
93 kernel,
94 streams,
95 w,
96 y_out,
97 post_out,
98 scratch,
99 num_tokens,
100 hidden_size,
101 hc.hc_mult as u32,
102 norm_eps,
103 stream,
104 ),
105 None => sinkhorn::hc_pre(
106 gpu,
107 kernel,
108 streams,
109 site.hc_fn,
110 site.hc_scale,
111 site.hc_base,
112 y_out,
113 post_out,
114 comb_out,
115 num_tokens,
116 hidden_size,
117 hc.hc_mult as u32,
118 hc.sinkhorn_iters as u32,
119 norm_eps,
120 hc.hc_eps,
121 stream,
122 ),
123 }
124}
125
126/// Inject the block output back into every stream. `out` may alias
127/// `residual`.
128///
129/// Takes the whole [`HcWeights`] rather than a site, because NEITHER variant's
130/// `hc_post` reads site weights — Sinkhorn consumes the `comb` its `hc_pre`
131/// emitted, low-rank the injection vector — so the layer's variant is the
132/// only thing being selected on.
133#[allow(clippy::too_many_arguments)]
134pub fn hc_post_site(
135 gpu: &dyn GpuBackend,
136 kernel: KernelHandle,
137 hc: &HcWeights,
138 block_out: DevicePtr,
139 residual: DevicePtr,
140 post: DevicePtr,
141 comb: DevicePtr,
142 out: DevicePtr,
143 num_tokens: u32,
144 hidden_size: u32,
145 stream: u64,
146) -> Result<()> {
147 match HcVariant::of(hc) {
148 // `post` IS the injection vector here; `comb` is not read.
149 HcVariant::LowRank => lowrank::hc_post_lowrank(
150 gpu,
151 kernel,
152 block_out,
153 residual,
154 post,
155 out,
156 num_tokens,
157 hidden_size,
158 hc.hc_mult as u32,
159 stream,
160 ),
161 HcVariant::Sinkhorn => sinkhorn::hc_post(
162 gpu,
163 kernel,
164 block_out,
165 residual,
166 post,
167 comb,
168 out,
169 num_tokens,
170 hidden_size,
171 hc.hc_mult as u32,
172 stream,
173 ),
174 }
175}
176
177/// The model-level final collapse before the LM head.
178///
179/// On Qwen this is ALSO the model's final normalization — the checkpoint
180/// ships no `model.norm.weight` because `hyper_connection_mixer`'s `hc_norm`
181/// plays that role.
182#[allow(clippy::too_many_arguments)]
183pub fn hc_head_site(
184 gpu: &dyn GpuBackend,
185 kernel: KernelHandle,
186 streams: DevicePtr,
187 head: &HcHeadWeights,
188 hc: &HcWeights,
189 y_out: DevicePtr,
190 scratch: DevicePtr,
191 num_tokens: u32,
192 hidden_size: u32,
193 norm_eps: f32,
194 stream: u64,
195) -> Result<()> {
196 match &head.lowrank {
197 Some(w) => lowrank::hc_head_lowrank(
198 gpu,
199 kernel,
200 streams,
201 w,
202 y_out,
203 scratch,
204 num_tokens,
205 hidden_size,
206 hc.hc_mult as u32,
207 norm_eps,
208 stream,
209 ),
210 None => sinkhorn::hc_head(
211 gpu,
212 kernel,
213 streams,
214 head.hc_fn,
215 head.hc_scale,
216 head.hc_base,
217 y_out,
218 num_tokens,
219 hidden_size,
220 hc.hc_mult as u32,
221 norm_eps,
222 hc.hc_eps,
223 stream,
224 ),
225 }
226}