Skip to content

Commit 5ae3436

Browse files
committed
[Gemm] Permute bias for concat_layout inside kernel if fp32
1 parent 7e47962 commit 5ae3436

8 files changed

Lines changed: 130 additions & 31 deletions

File tree

quack/epi_ops.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def begin(self, gemm, param, smem_tensor, ctx):
283283
tVgV = thr_copy.partition_S(gVec)
284284
tVsV = thr_copy.partition_D(smem_tensor)
285285
tVcV = thr_copy.partition_S(cute.make_identity_tensor(tile_dim))
286-
limit = min(mVec.shape[0] - coord_idx * tile_dim, tile_dim)
286+
limit = min(cute.size(mVec, mode=[0]) - coord_idx * tile_dim, tile_dim)
287287
pred = cute.make_rmem_tensor((1, cute.size(tVsV.shape[1])), Boolean)
288288
for m in cutlass.range(cute.size(tVsV.shape[1]), unroll_full=True):
289289
pred[0, m] = tVcV[0, m] < limit

quack/gemm_act.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
compile_gemm_kernel,
3838
)
3939
from quack.cache_utils import jit_cache
40+
import quack.layout_utils as layout_utils
4041
from quack.layout_utils import permute_gated_Cregs_b16
4142
from quack.activation import act_fn_map, gate_fn_map
4243
from quack.rounding import RoundingMode
@@ -67,6 +68,9 @@ def epi_to_underlying_arguments(self, args: EpilogueArguments, *, loc=None, ip=N
6768
self.cta_tile_shape_postact_mn = self.cta_tile_shape_mnk[:2]
6869
d = self._epi_ops_to_params_dict(args)
6970
d["act_fn"] = args.act_fn
71+
for key in ("mRowVecBroadcast", "mColVecBroadcast"):
72+
if key in self.concat_layout and key in d and d[key] is not None:
73+
d[key] = layout_utils.concat_to_interleave(d[key], 1)
7074
return self.EpilogueParams(**d)
7175

7276
# epi_get_tma_atoms, epi_smem_bytes_per_stage, epi_get_smem_struct,
@@ -210,6 +214,9 @@ def epi_to_underlying_arguments(
210214
)
211215
d = self._epi_ops_to_params_dict(args)
212216
d["act_fn"] = args.act_fn
217+
for key in ("mRowVecBroadcast", "mColVecBroadcast"):
218+
if key in self.concat_layout and key in d and d[key] is not None:
219+
d[key] = layout_utils.concat_to_interleave(d[key], 1)
213220
return self.EpilogueParams(**d)
214221

215222
@cute.jit

quack/gemm_default_epi.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from quack.gemm_sm100 import GemmSm100
1313
from quack.gemm_sm120 import GemmSm120
1414
from quack.rounding import RoundingMode
15+
import quack.layout_utils as layout_utils
1516
import quack.utils as utils
1617

1718

@@ -39,6 +40,9 @@ class EpilogueArguments(NamedTuple):
3940
def epi_to_underlying_arguments(self, args, *, loc=None, ip=None):
4041
self.rounding_mode = args.rounding_mode
4142
d = self._epi_ops_to_params_dict(args)
43+
for key in ("mRowVecBroadcast", "mColVecBroadcast"):
44+
if key in self.concat_layout and key in d and d[key] is not None:
45+
d[key] = layout_utils.concat_to_interleave(d[key], 1)
4246
return self.EpilogueParams(**d)
4347

4448
@cute.jit

quack/gemm_interface.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,13 @@ def _concat_interleave(t):
6060
dim = -2 if t.stride(-1) == 1 else -1
6161
return t.unflatten(dim, (2, t.shape[dim] // 2)).transpose(dim - 1, dim).flatten(dim - 1, dim)
6262

63+
6364
def _concat_interleave_bias(t):
6465
"""Interleave [gate; up] along last dim for bias vectors."""
6566
half = t.shape[-1] // 2
6667
return t.unflatten(-1, (2, half)).transpose(-2, -1).flatten(-2, -1)
6768

69+
6870
def default_config(device):
6971
cap = get_device_capacity(device)[0]
7072
if cap in [10, 11]:
@@ -204,16 +206,24 @@ def gemm_tuned(
204206
if dynamic_scheduler and get_device_capacity(A.device)[0] == 9
205207
else None
206208
)
209+
# Handle bias concat layout: transform "bias" key to kernel-level key or permute data.
210+
if concat_layout and "bias" in concat_layout:
211+
if bias is not None and bias.dtype.itemsize >= 4:
212+
# fp32: kernel permutes via layout; replace "bias" with the kernel-level key
213+
concat_layout = tuple("mRowVecBroadcast" if k == "bias" else k for k in concat_layout)
214+
else:
215+
# No bias or sub-fp32: strip "bias" from concat_layout; permute data if needed
216+
concat_layout = tuple(k for k in concat_layout if k != "bias")
217+
if bias is not None:
218+
bias = _concat_interleave_bias(bias)
207219
# When swap_ab, A↔B (out/C stay, but .mT flips their strides so the kernel
208220
# auto-detects the correct non-contiguous dim).
209-
_swap_map = {"A": "B", "B": "A", "out": "out", "C": "C"}
221+
_swap_map = {"A": "B", "B": "A", "out": "out", "C": "C", "mRowVecBroadcast": "mColVecBroadcast"}
210222
swapped_concat = (
211223
tuple(_swap_map.get(k, k) for k in concat_layout)
212224
if config.swap_ab and concat_layout
213225
else concat_layout
214226
)
215-
if concat_layout and "B" in concat_layout and bias is not None:
216-
bias = _concat_interleave_bias(bias)
217227
gemm_dispatch(
218228
A if not config.swap_ab else B,
219229
B if not config.swap_ab else A,
@@ -513,8 +523,8 @@ def gemm_ref(
513523
A = _concat_interleave(A)
514524
if "B" in concat_layout:
515525
B = _concat_interleave(B)
516-
if bias is not None:
517-
bias = _concat_interleave_bias(bias)
526+
if "bias" in concat_layout and bias is not None:
527+
bias = _concat_interleave_bias(bias)
518528
if cu_seqlens_m is None and cu_seqlens_k is None:
519529
fn = torch.bmm if A.ndim == 3 else torch.mm
520530
out = fn(A, B, out_dtype=out_dtype, out=out)
@@ -710,8 +720,8 @@ def gemm_add_ref(
710720
A = _concat_interleave(A)
711721
if "B" in concat_layout:
712722
B = _concat_interleave(B)
713-
if bias is not None:
714-
bias = _concat_interleave_bias(bias)
723+
if "bias" in concat_layout and bias is not None:
724+
bias = _concat_interleave_bias(bias)
715725
if "C" in concat_layout:
716726
C = _concat_interleave(C)
717727
if cu_seqlens_m is None and cu_seqlens_k is None:
@@ -1270,8 +1280,14 @@ def gemm_gated_tuned(
12701280
PostAct = postact_out
12711281
if bias is not None and bias.ndim == 1:
12721282
bias = bias.unsqueeze(0) # (L, N)
1273-
if concat_layout and "B" in concat_layout and bias is not None:
1274-
bias = _concat_interleave_bias(bias)
1283+
if concat_layout and "bias" in concat_layout:
1284+
if bias is not None and bias.dtype.itemsize >= 4:
1285+
bias_key = "mColVecBroadcast" if config.swap_ab else "mRowVecBroadcast"
1286+
concat_layout = tuple(bias_key if k == "bias" else k for k in concat_layout)
1287+
else:
1288+
concat_layout = tuple(k for k in concat_layout if k != "bias")
1289+
if bias is not None:
1290+
bias = _concat_interleave_bias(bias)
12751291
dynamic_scheduler = dynamic_scheduler or config.is_dynamic_persistent
12761292
tile_count_semaphore = (
12771293
torch.zeros(1, dtype=torch.int32, device=A.device)

quack/linear.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class _LinearGatedUntunedOps:
110110

111111

112112
class _LinearGatedConcatOps(_LinearGatedOps):
113-
matmul_fwd_fn = partial(gemm_gated, concat_layout=("B",))
113+
matmul_fwd_fn = partial(gemm_gated, concat_layout=("B", "bias"))
114114
matmul_bwd_dx = partial(gemm, dynamic_scheduler=True, concat_layout=("B",))
115115
matmul_bwd_dw = partial(gemm, dynamic_scheduler=True, concat_layout=("out",))
116116
matmul_bwd_dw_inplace = partial(
@@ -119,7 +119,7 @@ class _LinearGatedConcatOps(_LinearGatedOps):
119119

120120

121121
class _LinearGatedConcatUntunedOps(_LinearGatedUntunedOps):
122-
matmul_fwd_fn = partial(gemm_gated, tuned=False, concat_layout=("B",))
122+
matmul_fwd_fn = partial(gemm_gated, tuned=False, concat_layout=("B", "bias"))
123123
matmul_bwd_dx = partial(gemm, dynamic_scheduler=True, tuned=False, concat_layout=("B",))
124124
matmul_bwd_dw = partial(gemm, dynamic_scheduler=True, tuned=False, concat_layout=("out",))
125125
matmul_bwd_dw_inplace = partial(
@@ -274,10 +274,11 @@ def linear_gated_func(
274274

275275
class DActLinearFunc(torch.autograd.Function):
276276
@staticmethod
277-
def forward(ctx, preact, weight, x, activation, fuse_grad_accum, ops):
277+
def forward(ctx, preact, weight, x, activation, bias, fuse_grad_accum, ops):
278278
"""
279279
x: (..., in_features)
280280
weight: (out_features, in_features)
281+
bias: (out_features,) or None
281282
out: (..., out_features)
282283
Takes in an extra preact argument which is the pre-activation, to be used in the backward pass.
283284
"""
@@ -289,7 +290,7 @@ def forward(ctx, preact, weight, x, activation, fuse_grad_accum, ops):
289290
weight_og = weight
290291
batch_shape = x.shape[:-1]
291292
x = x.reshape(-1, x.shape[-1])
292-
out = ops.matmul_fwd_fn(x, weight.T)
293+
out = ops.matmul_fwd_fn(x, weight.T, bias=bias)
293294
# Store preact instead of x, we will recompute x (postact) in backward.
294295
# dpreact needs gemm_dact(dout, weight, preact) → needs both weight and preact.
295296
# dweight needs postact: if dpreact is also needed, postact comes from gemm_dact;
@@ -300,6 +301,8 @@ def forward(ctx, preact, weight, x, activation, fuse_grad_accum, ops):
300301
ctx, preact, weight, weight_og, needs_x_w_grad=(need_weight, need_preact)
301302
)
302303
ctx.activation = activation
304+
ctx.bias_dtype = bias.dtype if bias is not None else None
305+
ctx.compute_dbias = bias is not None and ctx.needs_input_grad[4]
303306
return out.reshape(*batch_shape, out.shape[-1])
304307

305308
@staticmethod
@@ -313,6 +316,7 @@ def backward(ctx, dout):
313316
preact, weight, weight_og = ctx.saved_tensors
314317
batch_shape = dout.shape[:-1]
315318
dout = _ensure_contiguous(dout.reshape(-1, dout.shape[-1]))
319+
dbias = dout.sum(0, dtype=ctx.bias_dtype) if ctx.compute_dbias else None
316320
if ctx.needs_input_grad[0]:
317321
# Need dpreact: gemm_dact(dout, weight, preact) → (dpreact, postact)
318322
preact = preact.reshape(-1, preact.shape[-1])
@@ -331,17 +335,17 @@ def backward(ctx, dout):
331335
dweight = linear_bwd_compute_weight_grad(
332336
ctx, dout, x, weight_og, ops.matmul_bwd_dw, ops.matmul_bwd_dw_inplace
333337
)
334-
return dpreact, dweight, None, None, None, None
338+
return dpreact, dweight, None, None, dbias, None, None
335339

336340

337-
def act_linear_func(preact, weight, x, activation, fuse_grad_accum=False, tuned=True):
341+
def act_linear_func(preact, weight, x, activation, bias=None, fuse_grad_accum=False, tuned=True):
338342
ops = _DActLinearOps if tuned else _DActLinearUntunedOps
339-
return DActLinearFunc.apply(preact, weight, x, activation, fuse_grad_accum, ops)
343+
return DActLinearFunc.apply(preact, weight, x, activation, bias, fuse_grad_accum, ops)
340344

341345

342-
def gated_linear_func(preact, weight, x, activation, fuse_grad_accum=False, tuned=True):
346+
def gated_linear_func(preact, weight, x, activation, bias=None, fuse_grad_accum=False, tuned=True):
343347
ops = _DGatedLinearOps if tuned else _DGatedLinearUntunedOps
344-
return DActLinearFunc.apply(preact, weight, x, activation, fuse_grad_accum, ops)
348+
return DActLinearFunc.apply(preact, weight, x, activation, bias, fuse_grad_accum, ops)
345349

346350

347351
class Linear(nn.Linear):

quack/mlp.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ def mlp_func(
207207
weight1,
208208
weight2,
209209
activation: str,
210+
bias1=None,
211+
bias2=None,
210212
fuse_grad_accum=False,
211213
tuned=True,
212214
recompute=False,
@@ -229,6 +231,7 @@ def mlp_func(
229231
x,
230232
weight1,
231233
activation,
234+
bias=bias1,
232235
store_preact=torch.is_grad_enabled(),
233236
fuse_grad_accum=fuse_grad_accum,
234237
tuned=tuned,
@@ -239,6 +242,7 @@ def mlp_func(
239242
weight2,
240243
postact,
241244
activation=activation,
245+
bias=bias2,
242246
fuse_grad_accum=fuse_grad_accum,
243247
tuned=tuned,
244248
)
@@ -292,9 +296,10 @@ def __init__(
292296
self.concat_layout = concat_layout
293297

294298
def forward(self, input: Tensor) -> Tensor:
299+
# Allow bias in the fused path during inference (fwd-only, no bwd).
300+
bias_ok = not torch.is_grad_enabled() or (self.fc1.bias is None and self.fc2.bias is None)
295301
if (
296-
self.fc1.bias is None
297-
and self.fc2.bias is None
302+
bias_ok
298303
and input.is_cuda
299304
and input.stride(-1) == 1
300305
and self.fc1.in_features % 8 == 0
@@ -306,6 +311,8 @@ def forward(self, input: Tensor) -> Tensor:
306311
self.fc1.weight,
307312
self.fc2.weight,
308313
activation=self.activation,
314+
bias1=self.fc1.bias,
315+
bias2=self.fc2.bias,
309316
fuse_grad_accum=self.fuse_grad_accum,
310317
tuned=self.tuned,
311318
recompute=self.recompute,

tests/test_linear.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,23 @@ def test_gemm(m, k, n, input_dtype, A_major, B_major, out_major, swap_ab):
109109
@pytest.mark.parametrize("B_major", ["k", "n"])
110110
@pytest.mark.parametrize("A_major", ["k", "m"])
111111
@pytest.mark.parametrize("batched", [False, True])
112+
@pytest.mark.parametrize("bias_dtype", [None, torch.float32, torch.bfloat16])
112113
@pytest.mark.parametrize("concat_tensor", ["A", "B", "out"])
113114
@pytest.mark.parametrize("input_dtype", [torch.bfloat16])
114115
@pytest.mark.parametrize("n", [1504, 2048])
115116
@pytest.mark.parametrize("k", [736, 1024])
116117
@pytest.mark.parametrize("m", [960])
117118
def test_gemm_concat_layout(
118-
m, k, n, input_dtype, concat_tensor, batched, A_major, B_major, out_major, swap_ab
119+
m, k, n, input_dtype, concat_tensor, bias_dtype, batched, A_major, B_major, out_major, swap_ab
119120
):
120121
"""Test concat_layout on each tensor with all major/stride combinations."""
122+
if bias_dtype is not None and batched:
123+
pytest.skip("batched + bias not supported")
121124
device = "cuda"
122125
torch.random.manual_seed(0)
123126
concat = (concat_tensor,)
127+
if bias_dtype is not None and concat_tensor == "B":
128+
concat = ("B", "bias")
124129
batch_shape = (3,) if batched else ()
125130
A = torch.randn((*batch_shape, m, k), device=device, dtype=input_dtype) / math.sqrt(k)
126131
if A_major == "m":
@@ -131,20 +136,21 @@ def test_gemm_concat_layout(
131136
out = torch.empty((*batch_shape, m, n), device=device, dtype=input_dtype)
132137
if out_major == "m":
133138
out = out.mT.contiguous().mT
139+
bias = torch.randn(n, device=device, dtype=bias_dtype) if bias_dtype is not None else None
134140
config = replace(default_config(torch.device(device)), swap_ab=swap_ab)
135-
gemm_tuned.fn(A, B, out, config=config, concat_layout=concat)
141+
gemm_tuned.fn(A, B, out, config=config, bias=bias, concat_layout=concat)
136142
# For ref: gemm_ref always produces n-major output and interleaves rows for concat="out".
137143
# When the kernel's out is m-major, the kernel interleaves columns instead.
138144
# Match by creating the ref with matching out major.
139145
if concat_tensor == "out" and out_major == "m":
140146
# Kernel interleaves columns. Ref: compute flat, then interleave columns.
141-
out_ref_flat = gemm_ref(A.float(), B.float())
147+
out_ref_flat = gemm_ref(A.float(), B.float(), bias=bias)
142148
out_ref = torch.cat([out_ref_flat[..., ::2], out_ref_flat[..., 1::2]], dim=-1)
143-
out_pt_flat = gemm_ref(A, B)
149+
out_pt_flat = gemm_ref(A, B, bias=bias)
144150
out_pt = torch.cat([out_pt_flat[..., ::2], out_pt_flat[..., 1::2]], dim=-1)
145151
else:
146-
out_ref = gemm_ref(A.float(), B.float(), concat_layout=concat)
147-
out_pt = gemm_ref(A, B, concat_layout=concat)
152+
out_ref = gemm_ref(A.float(), B.float(), bias=bias, concat_layout=concat)
153+
out_pt = gemm_ref(A, B, bias=bias, concat_layout=concat)
148154
assert (out - out_ref).abs().max() < 2 * (out_pt - out_ref).abs().max() + 1e-5
149155

150156

@@ -362,12 +368,12 @@ def test_gemm_add_inplace_alpha_beta(
362368
@pytest.mark.parametrize("store_preact", [True, False])
363369
@pytest.mark.parametrize("activation", ["swiglu", "swiglu_oai", "reglu", "geglu", "glu"])
364370
@pytest.mark.parametrize("input_dtype", [torch.bfloat16])
365-
@pytest.mark.parametrize("has_bias", [False, True])
371+
@pytest.mark.parametrize("bias_dtype", [None, torch.float32, torch.bfloat16])
366372
@pytest.mark.parametrize("out_features", [1504, 2048])
367373
@pytest.mark.parametrize("in_features", [736, 4096])
368374
@pytest.mark.parametrize("is_concat_layout_B", [False, True])
369375
def test_gemm_gated(
370-
in_features, out_features, has_bias, input_dtype, activation, store_preact, is_concat_layout_B
376+
in_features, out_features, bias_dtype, input_dtype, activation, store_preact, is_concat_layout_B
371377
):
372378
"""Test GEMM with gated activation forward computation."""
373379
device = "cuda"
@@ -381,8 +387,18 @@ def test_gemm_gated(
381387
/ math.sqrt(in_features)
382388
).requires_grad_()
383389
B = w.T
384-
concat = ("B",) if is_concat_layout_B else None
385-
bias = torch.randn(2 * out_features, device=device) if has_bias else None
390+
bias = (
391+
torch.randn(2 * out_features, device=device, dtype=bias_dtype)
392+
if bias_dtype is not None
393+
else None
394+
)
395+
concat = (
396+
("B", "bias")
397+
if is_concat_layout_B and bias_dtype is not None
398+
else ("B",)
399+
if is_concat_layout_B
400+
else None
401+
)
386402
preact, postact = gemm_gated(
387403
x,
388404
B,

0 commit comments

Comments
 (0)