fix lokr speed and convrot compile (#945)
This commit is contained in:
@@ -103,6 +103,9 @@ class LokrModule(ToolkitModuleMixin, nn.Module):
|
|||||||
self.use_w1 = False
|
self.use_w1 = False
|
||||||
self.use_w2 = False
|
self.use_w2 = False
|
||||||
self.can_merge_in = True
|
self.can_merge_in = True
|
||||||
|
# only the plain Linear path (no cp/Conv2d) gets the factorized forward;
|
||||||
|
# Conv2d and the cp branch keep using get_weight()/make_kron() below.
|
||||||
|
self._fast_linear = False
|
||||||
|
|
||||||
# avoid the weight property on quantized OstrisLinear: it dequantizes the
|
# avoid the weight property on quantized OstrisLinear: it dequantizes the
|
||||||
# whole weight just to answer .shape
|
# whole weight just to answer .shape
|
||||||
@@ -166,6 +169,10 @@ class LokrModule(ToolkitModuleMixin, nn.Module):
|
|||||||
out_l, out_k = factorization(out_dim, factor)
|
out_l, out_k = factorization(out_dim, factor)
|
||||||
# ((a, b), (c, d)), out_dim = a*c, in_dim = b*d
|
# ((a, b), (c, d)), out_dim = a*c, in_dim = b*d
|
||||||
shape = ((out_l, out_k), (in_m, in_n))
|
shape = ((out_l, out_k), (in_m, in_n))
|
||||||
|
# stash factor pair for the factorized (no full-kron) forward path
|
||||||
|
self._in_m, self._in_n = in_m, in_n
|
||||||
|
self._out_l, self._out_k = out_l, out_k
|
||||||
|
self._fast_linear = True
|
||||||
|
|
||||||
# smaller part. weight scale
|
# smaller part. weight scale
|
||||||
if decompose_both and lora_dim < max(shape[0][0], shape[1][0])/2:
|
if decompose_both and lora_dim < max(shape[0][0], shape[1][0])/2:
|
||||||
@@ -321,31 +328,85 @@ class LokrModule(ToolkitModuleMixin, nn.Module):
|
|||||||
return self.org_module[0].bias.data.detach()
|
return self.org_module[0].bias.data.detach()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _get_delta_factors(self):
|
||||||
|
"""(A, w2_or_w2a, w2_b_or_None) without ever combining the two kron
|
||||||
|
factors into a full-size matrix."""
|
||||||
|
A = self.lokr_w1 if self.use_w1 else self.lokr_w1_a @ self.lokr_w1_b # (out_l, in_m), always small
|
||||||
|
if self.use_w2:
|
||||||
|
return A, self.lokr_w2, None
|
||||||
|
else:
|
||||||
|
return A, self.lokr_w2_a, self.lokr_w2_b
|
||||||
|
|
||||||
|
def _call_forward_fast_linear(self, x):
|
||||||
|
"""Factorized LoKr delta for the plain Linear case (quantized or not).
|
||||||
|
Never materializes a (out_dim, in_dim) tensor: uses the kron
|
||||||
|
mixed-product identity plus the existing low-rank factoring of w2, so
|
||||||
|
both compute and the backward-pass gradient are O(rank) instead of
|
||||||
|
O(out_dim * in_dim)."""
|
||||||
|
if isinstance(x, QTensor) or isinstance(x, QBytesTensor):
|
||||||
|
x = x.dequantize()
|
||||||
|
orig_dtype = x.dtype
|
||||||
|
|
||||||
|
if getattr(self.org_module[0], "is_ostris_quantized", False):
|
||||||
|
# org_forward here does raw CUDA stream/event orchestration and
|
||||||
|
# in-place buffer swapping (manager_modules.py _mm_forward) to
|
||||||
|
# move the quantized weight onto the device -- that can't be
|
||||||
|
# traced by inductor, so break the graph at exactly this call.
|
||||||
|
base_out = torch._dynamo.disable(self.org_forward)(x)
|
||||||
|
else:
|
||||||
|
base_out = self.org_forward(x)
|
||||||
|
|
||||||
|
A, w2a, w2b = self._get_delta_factors()
|
||||||
|
# match the base path's compute dtype (usually bf16) rather than
|
||||||
|
# upcasting the (much larger) activation tensor to the fp32 master
|
||||||
|
# params -- keeps every intermediate here at the same footprint the
|
||||||
|
# old full-kron GEMM had, instead of doubling it.
|
||||||
|
compute_dtype = base_out.dtype
|
||||||
|
x_ = x.to(compute_dtype) if x.dtype != compute_dtype else x
|
||||||
|
A = A.to(compute_dtype)
|
||||||
|
w2a = w2a.to(compute_dtype)
|
||||||
|
if w2b is not None:
|
||||||
|
w2b = w2b.to(compute_dtype)
|
||||||
|
|
||||||
|
X = x_.unflatten(-1, (self._in_m, self._in_n))
|
||||||
|
|
||||||
|
if w2b is None:
|
||||||
|
# use_w2: w2a is the full (out_k, in_n) factor -- still far smaller
|
||||||
|
# than the full kron product, so a single einsum here is fine.
|
||||||
|
tmp = torch.einsum('...qs,os->...qo', X, w2a) # (..., in_m, out_k)
|
||||||
|
else:
|
||||||
|
# low-rank w2 = w2a @ w2b, rank << out_k, in_n: fold the rank
|
||||||
|
# factor through first so we never touch an (out_k, in_n) tensor.
|
||||||
|
tmp = torch.einsum('...qs,rs->...qr', X, w2b) # (..., in_m, rank)
|
||||||
|
tmp = torch.einsum('...qr,or->...qo', tmp, w2a) # (..., in_m, out_k)
|
||||||
|
|
||||||
|
# scale folded into A (not applied to the reduction output) to avoid
|
||||||
|
# an inductor lowering bug under torch.compile
|
||||||
|
delta = torch.einsum('...qo,pq->...po', tmp, A * self.scale) # (..., out_l, out_k)
|
||||||
|
delta = delta.flatten(-2, -1)
|
||||||
|
|
||||||
|
if self.training and self.rank_dropout:
|
||||||
|
# equivalent to the old row-mask on the full weight: zeroing a
|
||||||
|
# weight ROW is the same as zeroing that output channel post-matmul.
|
||||||
|
drop = torch.rand(delta.size(-1), device=delta.device) < self.rank_dropout
|
||||||
|
delta = delta * drop.to(delta.dtype)
|
||||||
|
|
||||||
|
multiplier = torch.mean(self.network_ref().torch_multiplier).to(compute_dtype)
|
||||||
|
delta = delta * multiplier
|
||||||
|
|
||||||
|
return (base_out + delta).to(orig_dtype)
|
||||||
|
|
||||||
def _call_forward(self, x):
|
def _call_forward(self, x):
|
||||||
|
if self._fast_linear:
|
||||||
|
return self._call_forward_fast_linear(x)
|
||||||
|
|
||||||
|
# legacy path: Conv2d / cp branch. Still materializes the full kron
|
||||||
|
# product -- TODO: extend the factorized path to cover these.
|
||||||
if isinstance(x, QTensor) or isinstance(x, QBytesTensor):
|
if isinstance(x, QTensor) or isinstance(x, QBytesTensor):
|
||||||
x = x.dequantize()
|
x = x.dequantize()
|
||||||
|
|
||||||
orig_dtype = x.dtype
|
orig_dtype = x.dtype
|
||||||
|
|
||||||
# for OstrisLinear (convrot-quantized), use the quantizer's own forward
|
|
||||||
# for the base computation and add the LoKr delta separately. this avoids
|
|
||||||
# materializing the full dequantized weight every forward, which bypasses
|
|
||||||
# the hardware fp4/int8 GEMM path and pegs the CPU with repeated
|
|
||||||
# dequantization + kron + matmul for every training step.
|
|
||||||
if getattr(self.org_module[0], "is_ostris_quantized", False):
|
|
||||||
base_out = torch._dynamo.disable(self.org_forward)(x)
|
|
||||||
lokr_weight = self.get_weight().to(dtype=orig_dtype)
|
|
||||||
multiplier = self.network_ref().torch_multiplier
|
|
||||||
multiplier = torch.mean(multiplier)
|
|
||||||
# bias is handled by org_forward (quantizer includes it)
|
|
||||||
delta_out = self.op(
|
|
||||||
x.to(dtype=lokr_weight.dtype),
|
|
||||||
lokr_weight.view(self.shape),
|
|
||||||
None,
|
|
||||||
**self.extra_args
|
|
||||||
)
|
|
||||||
return (base_out + delta_out * multiplier).to(orig_dtype)
|
|
||||||
|
|
||||||
orig_weight = self.get_orig_weight(x.device)
|
orig_weight = self.get_orig_weight(x.device)
|
||||||
lokr_weight = self.get_weight(orig_weight).to(dtype=orig_weight.dtype)
|
lokr_weight = self.get_weight(orig_weight).to(dtype=orig_weight.dtype)
|
||||||
multiplier = self.network_ref().torch_multiplier
|
multiplier = self.network_ref().torch_multiplier
|
||||||
|
|||||||
Reference in New Issue
Block a user