1use anyhow::Result;
14use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
15use metrale_gpu_runtime::kernel_args::KernelLaunch;
16
17#[derive(Clone, Copy)]
20pub struct Glm5NextMhcKernels {
21 pub hc_expand: KernelHandle,
26 pub hc_pre: KernelHandle,
29 pub hc_mix: KernelHandle,
32 pub hc_mix_bf16: KernelHandle,
36 pub hc_finish: KernelHandle,
39 pub hc_post: KernelHandle,
40 pub hc_head: KernelHandle,
41}
42
43pub const GLM5NEXT_MHC_MODULE: &str = "glm5next_mhc";
45
46impl Glm5NextMhcKernels {
47 pub fn resolve(gpu: &dyn GpuBackend) -> Result<Self> {
50 Ok(Self {
51 hc_expand: gpu.kernel(GLM5NEXT_MHC_MODULE, "glm5next_hc_expand")?,
52 hc_pre: gpu.kernel(GLM5NEXT_MHC_MODULE, "glm5next_hc_pre")?,
53 hc_mix: gpu.kernel(GLM5NEXT_MHC_MODULE, "glm5next_hc_mix")?,
54 hc_mix_bf16: metrale_model_layers::layers::try_kernel(
55 gpu,
56 GLM5NEXT_MHC_MODULE,
57 "glm5next_hc_mix_bf16",
58 ),
59 hc_finish: gpu.kernel(GLM5NEXT_MHC_MODULE, "glm5next_hc_finish")?,
60 hc_post: gpu.kernel(GLM5NEXT_MHC_MODULE, "glm5next_hc_post")?,
61 hc_head: gpu.kernel(GLM5NEXT_MHC_MODULE, "glm5next_hc_head")?,
62 })
63 }
64}
65
66pub fn glm_hc_expand(
69 gpu: &dyn GpuBackend,
70 kernel: KernelHandle,
71 hidden: DevicePtr,
72 streams: DevicePtr,
73 num_tokens: u32,
74 hidden_size: u32,
75 hc_mult: u32,
76 stream: u64,
77) -> Result<()> {
78 KernelLaunch::new(gpu, kernel)
79 .grid([num_tokens, 1, 1])
80 .block([256, 1, 1])
81 .arg_ptr(hidden)
82 .arg_ptr(streams)
83 .arg_u32(hidden_size)
84 .arg_u32(hc_mult)
85 .launch(stream)
86}
87
88pub fn hc_head_mean(
91 gpu: &dyn GpuBackend,
92 kernel: KernelHandle,
93 streams: DevicePtr,
94 y_out: DevicePtr,
95 num_tokens: u32,
96 hidden_size: u32,
97 hc_mult: u32,
98 stream: u64,
99) -> Result<()> {
100 KernelLaunch::new(gpu, kernel)
101 .grid([num_tokens, 1, 1])
102 .block([256, 1, 1])
103 .arg_ptr(streams)
104 .arg_ptr(y_out)
105 .arg_u32(hidden_size)
106 .arg_u32(hc_mult)
107 .launch(stream)
108}
109
110#[derive(Debug, Clone, Copy)]
113pub struct Glm5NextMhcSiteWeights {
114 pub hc_fn: DevicePtr,
117 pub hc_fn_bf16: bool,
119 pub hc_scale: DevicePtr,
121 pub hc_base: DevicePtr,
123 pub mix: DevicePtr,
126}
127
128pub const MHC_MIX_MAX_TOKENS: usize = 256;
130
131pub fn mhc_mix_max_tokens() -> usize {
135 static T: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
136 *T.get_or_init(|| {
137 let t = crate::glm5next_layer::prefill_rows().max(MHC_MIX_MAX_TOKENS);
138 if t != MHC_MIX_MAX_TOKENS {
139 tracing::warn!(
140 "GLM mHC `mix` scratch widened to {t} tokens (floor {MHC_MIX_MAX_TOKENS}) to \
141 match METRALE_GLM_PREFILL_ROWS"
142 );
143 }
144 t
145 })
146}
147
148#[allow(clippy::too_many_arguments)]
153pub fn glm_hc_pre(
154 gpu: &dyn GpuBackend,
155 kernels: &Glm5NextMhcKernels,
156 streams: DevicePtr,
157 w: &Glm5NextMhcSiteWeights,
158 y_out: DevicePtr,
159 post_out: DevicePtr,
160 comb_out: DevicePtr,
161 num_tokens: u32,
162 hidden_size: u32,
163 hc_mult: u32,
164 sinkhorn_iters: u32,
165 norm_eps: f32,
166 hc_eps: f32,
167 stream: u64,
168) -> Result<()> {
169 let mix_hc = (2 + hc_mult) * hc_mult;
170 let mix_cap = mhc_mix_max_tokens();
171 if num_tokens as usize > mix_cap {
172 anyhow::bail!(
173 "glm_hc_pre: {num_tokens} tokens exceeds the {mix_cap}-token `mix` scratch \
174 (floor {MHC_MIX_MAX_TOKENS}, widened to METRALE_GLM_PREFILL_ROWS). Do not launch \
175 past the allocation."
176 );
177 }
178 let mix_kernel = if w.hc_fn_bf16 && kernels.hc_mix_bf16.0 != 0 {
181 kernels.hc_mix_bf16
182 } else {
183 kernels.hc_mix
184 };
185 KernelLaunch::new(gpu, mix_kernel)
186 .grid([num_tokens, mix_hc, 1])
187 .block([256, 1, 1])
188 .arg_ptr(streams)
189 .arg_ptr(w.hc_fn)
190 .arg_ptr(w.mix)
191 .arg_u32(hidden_size)
192 .arg_u32(hc_mult)
193 .arg_f32(norm_eps)
194 .launch(stream)?;
195 KernelLaunch::new(gpu, kernels.hc_finish)
196 .grid([num_tokens, 1 + collapse_blocks(hidden_size), 1])
199 .block([256, 1, 1])
200 .arg_ptr(streams)
201 .arg_ptr(w.mix)
202 .arg_ptr(w.hc_scale)
203 .arg_ptr(w.hc_base)
204 .arg_ptr(y_out)
205 .arg_ptr(post_out)
206 .arg_ptr(comb_out)
207 .arg_u32(hidden_size)
208 .arg_u32(hc_mult)
209 .arg_u32(sinkhorn_iters)
210 .arg_f32(hc_eps)
211 .launch(stream)
212}
213
214#[allow(clippy::too_many_arguments)]
218pub fn glm_hc_post(
219 gpu: &dyn GpuBackend,
220 kernel: KernelHandle,
221 block_out: DevicePtr,
222 residual: DevicePtr,
223 post: DevicePtr,
224 comb: DevicePtr,
225 out: DevicePtr,
226 num_tokens: u32,
227 hidden_size: u32,
228 hc_mult: u32,
229 stream: u64,
230) -> Result<()> {
231 KernelLaunch::new(gpu, kernel)
232 .grid([num_tokens, collapse_blocks(hidden_size), 1])
233 .block([256, 1, 1])
234 .arg_ptr(block_out)
235 .arg_ptr(residual)
236 .arg_ptr(post)
237 .arg_ptr(comb)
238 .arg_ptr(out)
239 .arg_u32(hidden_size)
240 .arg_u32(hc_mult)
241 .launch(stream)
242}
243
244const fn collapse_blocks(hidden_size: u32) -> u32 {
248 if hidden_size < 256 {
249 1
250 } else {
251 hidden_size.div_ceil(256)
252 }
253}
254
255pub fn mix_hc(hc_mult: usize) -> usize {
258 (2 + hc_mult) * hc_mult
259}
260
261#[cfg(test)]
262mod mhc_shape_tests {
263 use super::*;
264
265 #[test]
267 fn mix_hc_splits_into_pre_post_and_comb() {
268 for hc in 1usize..=8 {
269 assert_eq!(mix_hc(hc), hc + hc + hc * hc, "pre + post + comb rows");
270 }
271 assert_eq!(mix_hc(2), 8);
272 }
273
274 #[test]
277 fn the_kernels_mix_bound_is_hc_mult_four() {
278 assert_eq!(mix_hc(4), 24, "GLM_HC_MAX_MIX in glm5next_mhc.cu");
279 assert!(mix_hc(5) > 24, "hc_mult 5 would exceed the kernel's bound");
280 }
281
282 #[test]
285 fn the_mix_bound_never_goes_below_the_shipped_floor() {
286 assert_eq!(mhc_mix_max_tokens(), MHC_MIX_MAX_TOKENS);
287 assert!(mhc_mix_max_tokens() >= MHC_MIX_MAX_TOKENS);
288 }
289
290 #[test]
293 fn the_mix_scratch_costs_megabytes_not_gigabytes() {
294 let total = |tokens: usize| tokens * mix_hc(4) * 4 * 90;
295 assert_eq!(
296 total(256),
297 2_211_840,
298 "2.2 MB for the whole model at the floor"
299 );
300 assert_eq!(total(1024), 8_847_360, "8.8 MB at a 1024-row sub-chunk");
301 }
302}