Skip to content

Commit 51168c5

Browse files
committed
feat(paged): fused gated RMSNorm + SiLU gate-mul CUDA op (patch 0044)
The Qwen3.6 gated-DeltaNet output norm self.norm(core_attn_out, z) (qwen35 / qwen35moe build_norm_gated) runs as (rms_norm(x) * w) * silu(z): on CUDA that was rms_norm_mul + silu_mul, two fused launches with the normalized intermediate round-tripping through HBM. Fuse the whole chain into one kernel so it stays in registers. This is the gated-RMSNorm fusion the vLLM decode-gap analysis ranked ggml-org#1 (the easy, bit-exact prefill win), a direct sibling of patch 0042 (add-RMSNorm). The chain is NOT naturally consecutive in the graph: the gate z-projection (a MUL_MAT) is scheduled between the weight MUL and the SILU, so the default mul(normalized, silu(z)) order leaves a GEMM between them and cannot be fused. build_norm_gated now emits the gate multiply as mul(silu(z), normalized) (commutative, so bit-exact), which lays the chain out as the consecutive subgraph { SILU, RMS_NORM, MUL, MUL } that ggml-cuda can fuse. - New kernel rms_norm_gate_mul_f32 (ggml/src/ggml-cuda/norm.cu): same block_reduce<SUM> over x^2, same 256/1024 block-size thresholds and rsqrtf(mean+eps) as rms_norm / patch 0042; the final write computes dst = scale * x * w * silu(z) with silu(z) = z/(1+expf(-z)) (the exact ggml_cuda_op_silu_single form). w (the RMS weight) and z (the gate) both broadcast via the packed-modulo helper. - ggml_cuda_can_fuse recognizes { GGML_OP_UNARY(SILU), RMS_NORM, MUL, MUL } via ggml_can_fuse_subgraph with the final MUL as the only output (the SILU reads an external gate; RMS_NORM and the weight MUL are single-use within). - Gated by LLAMA_FUSE_GATE_RMSNORM (default ON) for a clean single-build A/B; OFF keeps the original operand order AND the unfused kernels, so OFF is byte- and kernel-identical to the pre-patch path. BIT-EXACT (per-path canonical greedy md5, n=48 --temp 0 --seed 1): dense q36-27b-nvfp4 : 5951a5b4d624ce891e22ab5fca9bc439 (ON == OFF == canonical, paged and non-paged) MoE q36-35b-a3b : 8cb0ce23777bf55f92f63d0292c756b0 (ON == OFF == canonical, paged) Multiply is commutative, so ((scale*x)*w)*silu(z) is byte-identical to the unfused silu(z)*((scale*x)*w); the sum(x^2) reduction and rsqrt scale are unchanged. test-backend-ops 12979/12979 (CUDA0 vs CPU). PROFILE (dense prefill, nsys --cuda-graph-trace=node, npp512 ntg4 npl8): rms_norm_f32<256,1,0> 560 -> 224 launches unary_gated_op_kernel<op_silu> 784 -> 448 launches rms_norm_gate_mul_f32 (new) 336 launches / 69.7M ns -> the 336 gated-norm rms_norm_mul + 336 silu_mul launches (672) fold into 336 fused launches, removing the normalized HBM round-trip. S_PP (npp512 ntg4 npl32, 3x interleaved A/B, every ON beats every OFF): dense q36-27b : 1002.5 -> 1013.4 t/s (+1.1%, ~+10 us/tok) MoE q36-35b : 2626.9 -> 2651.8 t/s (+0.9%) Assisted-by: Claude:opus-4.8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 2c32ab8 commit 51168c5

5 files changed

Lines changed: 320 additions & 0 deletions

File tree

ggml/src/ggml-cuda/ggml-cuda.cu

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3816,6 +3816,60 @@ static bool ggml_cuda_can_fuse(const struct ggml_cgraph * cgraph,
38163816
return true;
38173817
}
38183818

3819+
// Fused gated RMS norm: SiLU gate multiply over (RMS norm * weight), the
3820+
// gated-DeltaNet output norm `out = (rms_norm(x) * w) * silu(z)` of the Qwen3.6
3821+
// hybrid models (qwen35 / qwen35moe build_norm_gated). The model emits the gate
3822+
// multiply as mul(silu(z), normalized) (default; see LLAMA_FUSE_GATE_RMSNORM in
3823+
// build_norm_gated) so the chain forms the consecutive subgraph
3824+
// { SILU, RMS_NORM, MUL, MUL } - the gate z-projection is scheduled before the
3825+
// SILU, so the natural mul(normalized, silu) order leaves a GEMM between the
3826+
// weight MUL and the SILU and cannot be fused. The SILU (node_idx) reads an
3827+
// external gate and the final gate MUL (node_idx + 3) feeds the o_proj, so mark
3828+
// node_idx + 3 as the only output; the RMS_NORM (node_idx + 1) and weight MUL
3829+
// (node_idx + 2) are single-use within the subgraph.
3830+
std::initializer_list<enum ggml_op> rms_norm_gate_mul_ops = { GGML_OP_UNARY, GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_MUL };
3831+
if (is_equal(rms_norm_gate_mul_ops, ops) && unary_ops.size() == 1 && unary_ops.begin()[0] == GGML_UNARY_OP_SILU &&
3832+
ggml_can_fuse_subgraph(cgraph, node_idx, ops, { node_idx + 3 })) {
3833+
const ggml_tensor * silu = cgraph->nodes[node_idx];
3834+
const ggml_tensor * rms_norm = cgraph->nodes[node_idx + 1];
3835+
const ggml_tensor * mul = cgraph->nodes[node_idx + 2];
3836+
const ggml_tensor * gate_mul = cgraph->nodes[node_idx + 3];
3837+
3838+
if (ggml_get_unary_op(silu) != GGML_UNARY_OP_SILU) {
3839+
return false;
3840+
}
3841+
// The weight MUL must consume the RMS norm output; the gate MUL must
3842+
// consume both the weight MUL and the SILU output.
3843+
if (mul->src[0] != rms_norm && mul->src[1] != rms_norm) {
3844+
return false;
3845+
}
3846+
if ((gate_mul->src[0] != mul && gate_mul->src[1] != mul) ||
3847+
(gate_mul->src[0] != silu && gate_mul->src[1] != silu)) {
3848+
return false;
3849+
}
3850+
// All operands F32 (rms norm / fused mul / silu kernel only support F32).
3851+
if (rms_norm->src[0]->type != GGML_TYPE_F32 || rms_norm->type != GGML_TYPE_F32 ||
3852+
mul->src[0]->type != GGML_TYPE_F32 || mul->src[1]->type != GGML_TYPE_F32 || mul->type != GGML_TYPE_F32 ||
3853+
silu->src[0]->type != GGML_TYPE_F32 || silu->type != GGML_TYPE_F32 ||
3854+
gate_mul->src[0]->type != GGML_TYPE_F32 || gate_mul->src[1]->type != GGML_TYPE_F32 ||
3855+
gate_mul->type != GGML_TYPE_F32) {
3856+
return false;
3857+
}
3858+
// If rms_norm is the B operand of the weight mul, broadcast of A is unsupported.
3859+
if (rms_norm == mul->src[1] && !ggml_are_same_shape(mul->src[0], rms_norm)) {
3860+
return false;
3861+
}
3862+
// The fused kernel reads contiguous rows for the norm input, the weight,
3863+
// and the gate, and writes a contiguous output.
3864+
if (!ggml_is_contiguous_rows(rms_norm->src[0]) ||
3865+
!ggml_is_contiguous_rows(mul->src[0]) || !ggml_is_contiguous_rows(mul->src[1]) ||
3866+
!ggml_is_contiguous_rows(silu->src[0]) ||
3867+
!ggml_is_contiguous_rows(gate_mul->src[0]) || !ggml_is_contiguous_rows(gate_mul->src[1])) {
3868+
return false;
3869+
}
3870+
return true;
3871+
}
3872+
38193873
if (!ggml_can_fuse(cgraph, node_idx, ops)) {
38203874
return false;
38213875
}
@@ -4350,6 +4404,19 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph
43504404
return 2;
43514405
}
43524406

4407+
// Fused gated RMS norm: RMS norm + weight multiply + SiLU-gated multiply
4408+
// (bit-exact). The Qwen3.6 gated-DeltaNet output norm. Default ON; set
4409+
// LLAMA_FUSE_GATE_RMSNORM=0 for a clean A/B against the unfused path.
4410+
static const bool fuse_gate_rmsnorm = [] {
4411+
const char * e = getenv("LLAMA_FUSE_GATE_RMSNORM");
4412+
return e == nullptr || atoi(e) != 0;
4413+
}();
4414+
if (fuse_gate_rmsnorm &&
4415+
ggml_cuda_can_fuse(cgraph, i, { GGML_OP_UNARY, GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_MUL }, { GGML_UNARY_OP_SILU })) {
4416+
ggml_cuda_op_rms_norm_gate_mul(*cuda_ctx, cgraph->nodes[i + 1], cgraph->nodes[i + 2], node, cgraph->nodes[i + 3]);
4417+
return 3;
4418+
}
4419+
43534420
if (ggml_cuda_can_fuse(cgraph, i, { GGML_OP_RMS_NORM, GGML_OP_MUL, GGML_OP_ADD }, {})) {
43544421
ggml_cuda_op_rms_norm_fused_add(*cuda_ctx, node, cgraph->nodes[i + 1], cgraph->nodes[i + 2]);
43554422
return 2;

ggml/src/ggml-cuda/norm.cu

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,95 @@ static __global__ void rms_norm_pre_add_mul_f32(const float * a,
235235
}
236236
}
237237

238+
// Fused gated RMS norm: RMS norm + weight multiply + SiLU gate multiply.
239+
// dst = (rsqrt(mean(x^2)+eps) * x * w) * silu(z) with silu(z) = z/(1+expf(-z))
240+
// This is the gated-DeltaNet output norm `self.norm(core_attn_out, z)` of the
241+
// Qwen3.6 hybrid models (build_norm_gated): rms_norm(x) scaled by the per-head
242+
// ssm_norm weight `w`, then gated by silu of the gate activation `z`. Unfused it
243+
// runs as rms_norm_mul (scale*x*w) -> silu(z) -> mul; fusing it keeps the
244+
// normalized intermediate in registers so it never round-trips to HBM.
245+
//
246+
// Bit-exactness: the sum(x^2) reduction uses the same block_reduce<SUM> with the
247+
// same 256/1024 block-size thresholds and the same rsqrtf(mean+eps) as rms_norm,
248+
// the weight multiply reproduces rms_norm_mul's `scale*x[col]*w[col]` order, and
249+
// silu reuses the exact `z/(1+expf(-z))` of ggml_cuda_op_silu_single. Float
250+
// multiply is commutative, so `(scale*x*w) * silu(z)` is byte-identical to the
251+
// unfused `mul(rms_norm_mul, silu(z))` (whether or not silu+mul was itself fused).
252+
// `w` (the RMS weight) and `z` (the gate) both broadcast via the packed-modulo path.
253+
template <int block_size>
254+
static __global__ void rms_norm_gate_mul_f32(const float * x,
255+
float * dst,
256+
const int ncols,
257+
const int64_t stride_row,
258+
const int64_t stride_channel,
259+
const int64_t stride_sample,
260+
const float eps,
261+
const float * mul,
262+
const int64_t mul_stride_row,
263+
const int64_t mul_stride_channel,
264+
const int64_t mul_stride_sample,
265+
const uint3 mul_ncols_packed,
266+
const uint3 mul_nrows_packed,
267+
const uint3 mul_nchannels_packed,
268+
const uint3 mul_nsamples_packed,
269+
const float * gate,
270+
const int64_t gate_stride_row,
271+
const int64_t gate_stride_channel,
272+
const int64_t gate_stride_sample,
273+
const uint3 gate_ncols_packed,
274+
const uint3 gate_nrows_packed,
275+
const uint3 gate_nchannels_packed,
276+
const uint3 gate_nsamples_packed) {
277+
ggml_cuda_pdl_lc();
278+
const int nrows = gridDim.x;
279+
const int nchannels = gridDim.y;
280+
281+
const int row = blockIdx.x;
282+
const int channel = blockIdx.y;
283+
const int sample = blockIdx.z;
284+
const int tid = threadIdx.x;
285+
286+
x += sample*stride_sample + channel*stride_channel + row*stride_row;
287+
// dst is laid out contiguously by the scheduler for the (final) MUL output
288+
dst += ((sample*nchannels + channel)*nrows + row)*ncols;
289+
290+
{
291+
const uint32_t mul_row = fastmodulo(row, mul_nrows_packed);
292+
const uint32_t mul_channel = fastmodulo(channel, mul_nchannels_packed);
293+
const uint32_t mul_sample = fastmodulo(sample, mul_nsamples_packed);
294+
mul += mul_sample * mul_stride_sample + mul_channel * mul_stride_channel + mul_row * mul_stride_row;
295+
}
296+
{
297+
const uint32_t gate_row = fastmodulo(row, gate_nrows_packed);
298+
const uint32_t gate_channel = fastmodulo(channel, gate_nchannels_packed);
299+
const uint32_t gate_sample = fastmodulo(sample, gate_nsamples_packed);
300+
gate += gate_sample * gate_stride_sample + gate_channel * gate_stride_channel + gate_row * gate_stride_row;
301+
}
302+
303+
float tmp = 0.0f; // partial sum for thread in warp
304+
305+
ggml_cuda_pdl_sync();
306+
for (int col = tid; col < ncols; col += block_size) {
307+
const float xi = x[col];
308+
tmp += xi * xi;
309+
}
310+
311+
// sum up partial sums
312+
extern __shared__ float s_sum[];
313+
tmp = block_reduce<block_reduce_method::SUM, block_size>(tmp, s_sum);
314+
315+
const float mean = tmp / ncols;
316+
const float scale = rsqrtf(mean + eps);
317+
318+
for (int col = tid; col < ncols; col += block_size) {
319+
const int mul_col = fastmodulo(col, mul_ncols_packed);
320+
const int gate_col = fastmodulo(col, gate_ncols_packed);
321+
const float zi = gate[gate_col];
322+
const float silu_z = zi / (1.0f + expf(-zi));
323+
dst[col] = scale * x[col] * mul[mul_col] * silu_z;
324+
}
325+
}
326+
238327
template <int block_size>
239328
static __global__ void rms_norm_back_f32(
240329
const float * grad, const float * xf, float * dst, const int ncols, const float eps) {
@@ -532,6 +621,65 @@ static void rms_norm_pre_add_mul_f32_cuda(const float * a,
532621
}
533622
}
534623

624+
static void rms_norm_gate_mul_f32_cuda(const float * x,
625+
float * dst,
626+
const int ncols,
627+
const int nrows,
628+
const int nchannels,
629+
const int nsamples,
630+
const int64_t stride_row,
631+
const int64_t stride_channel,
632+
const int64_t stride_sample,
633+
const float * mul,
634+
const int64_t mul_stride_row,
635+
const int64_t mul_stride_channel,
636+
const int64_t mul_stride_sample,
637+
const uint32_t mul_ncols,
638+
const uint32_t mul_nrows,
639+
const uint32_t mul_nchannels,
640+
const uint32_t mul_nsamples,
641+
const float * gate,
642+
const int64_t gate_stride_row,
643+
const int64_t gate_stride_channel,
644+
const int64_t gate_stride_sample,
645+
const uint32_t gate_ncols,
646+
const uint32_t gate_nrows,
647+
const uint32_t gate_nchannels,
648+
const uint32_t gate_nsamples,
649+
const float eps,
650+
cudaStream_t stream) {
651+
const dim3 blocks_num(nrows, nchannels, nsamples);
652+
GGML_ASSERT(mul != nullptr);
653+
GGML_ASSERT(gate != nullptr);
654+
const uint3 mul_ncols_packed = init_fastdiv_values(mul_ncols);
655+
const uint3 mul_nrows_packed = init_fastdiv_values(mul_nrows);
656+
const uint3 mul_nchannels_packed = init_fastdiv_values(mul_nchannels);
657+
const uint3 mul_nsamples_packed = init_fastdiv_values(mul_nsamples);
658+
const uint3 gate_ncols_packed = init_fastdiv_values(gate_ncols);
659+
const uint3 gate_nrows_packed = init_fastdiv_values(gate_nrows);
660+
const uint3 gate_nchannels_packed = init_fastdiv_values(gate_nchannels);
661+
const uint3 gate_nsamples_packed = init_fastdiv_values(gate_nsamples);
662+
if (ncols < 1024) {
663+
const dim3 block_dims(256, 1, 1);
664+
const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params{blocks_num, block_dims, block_dims.x > WARP_SIZE ? 32 * sizeof(float) : 0, stream};
665+
ggml_cuda_kernel_launch(rms_norm_gate_mul_f32<256>, launch_params,
666+
x, dst, ncols, stride_row, stride_channel, stride_sample, eps,
667+
mul, mul_stride_row, mul_stride_channel, mul_stride_sample,
668+
mul_ncols_packed, mul_nrows_packed, mul_nchannels_packed, mul_nsamples_packed,
669+
gate, gate_stride_row, gate_stride_channel, gate_stride_sample,
670+
gate_ncols_packed, gate_nrows_packed, gate_nchannels_packed, gate_nsamples_packed);
671+
} else {
672+
const dim3 block_dims(1024, 1, 1);
673+
const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params{blocks_num, block_dims, block_dims.x > WARP_SIZE ? 32 * sizeof(float) : 0, stream};
674+
ggml_cuda_kernel_launch(rms_norm_gate_mul_f32<1024>, launch_params,
675+
x, dst, ncols, stride_row, stride_channel, stride_sample, eps,
676+
mul, mul_stride_row, mul_stride_channel, mul_stride_sample,
677+
mul_ncols_packed, mul_nrows_packed, mul_nchannels_packed, mul_nsamples_packed,
678+
gate, gate_stride_row, gate_stride_channel, gate_stride_sample,
679+
gate_ncols_packed, gate_nrows_packed, gate_nchannels_packed, gate_nsamples_packed);
680+
}
681+
}
682+
535683
static void rms_norm_back_f32_cuda(const float * grad, const float * xf, float * dst, const int ncols, const int nrows, const float eps, cudaStream_t stream) {
536684
if (ncols < 1024) {
537685
const dim3 block_dims(WARP_SIZE, 1, 1);
@@ -843,6 +991,73 @@ void ggml_cuda_op_rms_norm_pre_add_mul(ggml_backend_cuda_context & ctx,
843991
eps, stream);
844992
}
845993

994+
void ggml_cuda_op_rms_norm_gate_mul(ggml_backend_cuda_context & ctx,
995+
ggml_tensor * rms_norm_tensor,
996+
ggml_tensor * mul_tensor,
997+
ggml_tensor * silu_tensor,
998+
ggml_tensor * gate_mul_tensor) {
999+
// mul = rms_norm(x) * w ; silu = silu(z) ; gate_mul = mul * silu
1000+
GGML_ASSERT(mul_tensor->src[0] == rms_norm_tensor || mul_tensor->src[1] == rms_norm_tensor);
1001+
GGML_ASSERT(gate_mul_tensor->src[0] == silu_tensor || gate_mul_tensor->src[1] == silu_tensor);
1002+
1003+
const ggml_tensor * x_src = rms_norm_tensor->src[0];
1004+
const ggml_tensor * w_src = (mul_tensor->src[0] == rms_norm_tensor) ? mul_tensor->src[1] : mul_tensor->src[0];
1005+
const ggml_tensor * gate_src = silu_tensor->src[0];
1006+
1007+
float eps = 0.0f;
1008+
memcpy(&eps, rms_norm_tensor->op_params, sizeof(float));
1009+
GGML_ASSERT(eps >= 0.0f);
1010+
1011+
const float * x_d = (const float *) x_src->data;
1012+
const float * w_d = (const float *) w_src->data;
1013+
const float * gate_d = (const float *) gate_src->data;
1014+
float * dst_d = (float *) gate_mul_tensor->data;
1015+
cudaStream_t stream = ctx.stream();
1016+
1017+
GGML_ASSERT(x_src->type == GGML_TYPE_F32);
1018+
GGML_ASSERT(w_src->type == GGML_TYPE_F32);
1019+
GGML_ASSERT(gate_src->type == GGML_TYPE_F32);
1020+
GGML_ASSERT(rms_norm_tensor->type == GGML_TYPE_F32);
1021+
GGML_ASSERT(mul_tensor->type == GGML_TYPE_F32);
1022+
GGML_ASSERT(silu_tensor->type == GGML_TYPE_F32);
1023+
GGML_ASSERT(gate_mul_tensor->type == GGML_TYPE_F32);
1024+
1025+
const int64_t ne00 = rms_norm_tensor->ne[0];
1026+
const int64_t ne01 = rms_norm_tensor->ne[1];
1027+
const int64_t ne02 = rms_norm_tensor->ne[2];
1028+
const int64_t ne03 = rms_norm_tensor->ne[3];
1029+
1030+
// x (the rms-norm input) strides; cols must be contiguous
1031+
const size_t ts0 = ggml_type_size(x_src->type);
1032+
GGML_ASSERT(x_src->nb[0] == ts0);
1033+
const int64_t s01 = x_src->nb[1] / ts0;
1034+
const int64_t s02 = x_src->nb[2] / ts0;
1035+
const int64_t s03 = x_src->nb[3] / ts0;
1036+
1037+
// weight (the RMS scale) strides + broadcast extents
1038+
const size_t ts_mul = ggml_type_size(w_src->type);
1039+
GGML_ASSERT(w_src->nb[0] == ts_mul);
1040+
const int64_t mul_s01 = w_src->nb[1] / ts_mul;
1041+
const int64_t mul_s02 = w_src->nb[2] / ts_mul;
1042+
const int64_t mul_s03 = w_src->nb[3] / ts_mul;
1043+
1044+
// gate (the silu activation) strides + broadcast extents
1045+
const size_t ts_gate = ggml_type_size(gate_src->type);
1046+
GGML_ASSERT(gate_src->nb[0] == ts_gate);
1047+
const int64_t gate_s01 = gate_src->nb[1] / ts_gate;
1048+
const int64_t gate_s02 = gate_src->nb[2] / ts_gate;
1049+
const int64_t gate_s03 = gate_src->nb[3] / ts_gate;
1050+
1051+
rms_norm_gate_mul_f32_cuda(x_d, dst_d,
1052+
ne00, ne01, ne02, ne03,
1053+
/*s00*/ s01, s02, s03,
1054+
w_d, /*mul_s00*/ mul_s01, mul_s02, mul_s03,
1055+
w_src->ne[0], w_src->ne[1], w_src->ne[2], w_src->ne[3],
1056+
gate_d, /*gate_s00*/ gate_s01, gate_s02, gate_s03,
1057+
gate_src->ne[0], gate_src->ne[1], gate_src->ne[2], gate_src->ne[3],
1058+
eps, stream);
1059+
}
1060+
8461061
void ggml_cuda_op_rms_norm_back(ggml_backend_cuda_context & ctx, ggml_tensor * dst) {
8471062
const ggml_tensor * grad = dst->src[0]; // gradients
8481063
const ggml_tensor * src0f = dst->src[1]; // src0 from forward pass

ggml/src/ggml-cuda/norm.cuh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ void ggml_cuda_op_rms_norm_pre_add_mul(ggml_backend_cuda_context & ctx,
1717
ggml_tensor * add_tensor,
1818
ggml_tensor * rms_norm_tensor,
1919
ggml_tensor * mul_tensor);
20+
void ggml_cuda_op_rms_norm_gate_mul(ggml_backend_cuda_context & ctx,
21+
ggml_tensor * rms_norm_tensor,
22+
ggml_tensor * mul_tensor,
23+
ggml_tensor * silu_tensor,
24+
ggml_tensor * gate_mul_tensor);
25+
2026

2127
void ggml_cuda_op_rms_norm_back(ggml_backend_cuda_context & ctx, ggml_tensor * dst);
2228

src/models/qwen35.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "models.h"
2+
#include <cstdlib>
23
#include "llama-memory-recurrent.h"
34

45
void llama_model_qwen35::load_arch_hparams(llama_model_loader & ml) {
@@ -251,6 +252,21 @@ ggml_tensor * llama_model_qwen35::graph::build_norm_gated(
251252
ggml_tensor * normalized = build_norm(input, weights, nullptr, LLM_NORM_RMS, layer);
252253
ggml_tensor * gated_silu = ggml_silu(ctx0, gate);
253254

255+
// Emit the gate multiply as mul(silu(z), normalized) so the gated-DeltaNet
256+
// output-norm chain forms the consecutive subgraph { SILU, RMS_NORM, MUL, MUL }
257+
// that the CUDA backend fuses into one rms_norm_gate_mul kernel (the normalized
258+
// intermediate then never round-trips to HBM). The gate z-projection is scheduled
259+
// before the SILU, so the natural mul(normalized, silu) order leaves a GEMM
260+
// between the weight MUL and the SILU and is not fusable. Multiplication is
261+
// commutative, so this is bit-exact vs mul(normalized, silu).
262+
// LLAMA_FUSE_GATE_RMSNORM=0 keeps the original operand order (kernel fusion off).
263+
static const bool fuse_gate_rmsnorm = [] {
264+
const char * e = getenv("LLAMA_FUSE_GATE_RMSNORM");
265+
return e == nullptr || atoi(e) != 0;
266+
}();
267+
if (fuse_gate_rmsnorm) {
268+
return ggml_mul(ctx0, gated_silu, normalized);
269+
}
254270
return ggml_mul(ctx0, normalized, gated_silu);
255271
}
256272

0 commit comments

Comments
 (0)