@@ -289,6 +289,61 @@ def linear_dq8ca_q4gsw(
289289lib .impl (name , linear_q4gsw , "CompositeExplicitAutograd" )
290290linear_qc4w_op = getattr (getattr (torch .ops , namespace ), name )
291291
292+
293+ # Backward of linear_q4gsw wrt input (for on-device LoRA training through a frozen
294+ # 4-bit base): d_x = d_out @ dequant(W). Reference impl extracts dequant(W) via the
295+ # forward on an identity so it is layout-agnostic; the runtime dispatches this op to
296+ # the tiled q4gsw_backward WGSL kernel (contracts over N).
297+ def linear_q4gsw_backward_impl (
298+ d_out : torch .Tensor ,
299+ weights : torch .Tensor ,
300+ weight_scales : torch .Tensor ,
301+ group_size : int ,
302+ ) -> torch .Tensor :
303+ in_features = int (weights .shape [1 ]) * 2
304+ eye = torch .eye (in_features , dtype = d_out .dtype , device = d_out .device )
305+ w_t = linear_q4gsw (eye , weights , weight_scales , group_size ) # [in, out]
306+ return d_out @ w_t .t () # [M, out] @ [out, in] = [M, in]
307+
308+
309+ def linear_q4gsw_backward_meta (
310+ d_out : torch .Tensor ,
311+ weights : torch .Tensor ,
312+ weight_scales : torch .Tensor ,
313+ group_size : int ,
314+ ) -> torch .Tensor :
315+ return d_out .new_empty (d_out .shape [:- 1 ] + (int (weights .shape [1 ]) * 2 ,))
316+
317+
318+ name = "linear_q4gsw_backward"
319+ lib .define (
320+ f"{ name } (Tensor d_out, Tensor weights, Tensor weight_scales, int group_size) -> Tensor"
321+ )
322+ lib .impl (name , linear_q4gsw_backward_impl , "CompositeExplicitAutograd" )
323+ lib .impl (name , linear_q4gsw_backward_meta , "Meta" )
324+ linear_q4gsw_backward_op = getattr (getattr (torch .ops , namespace ), name )
325+
326+
327+ def linear_q4gsw_setup_context (ctx , inputs , output ) -> None :
328+ _x , weights , weight_scales , group_size , _bias = inputs
329+ ctx .save_for_backward (weights , weight_scales )
330+ ctx .group_size = group_size
331+
332+
333+ def linear_q4gsw_backward (ctx , grad_out ):
334+ weights , weight_scales = ctx .saved_tensors
335+ d_x = torch .ops .et_vk .linear_q4gsw_backward (
336+ grad_out , weights , weight_scales , ctx .group_size
337+ )
338+ return d_x , None , None , None , None # grads for (x, weights, scales, group_size, bias)
339+
340+
341+ torch .library .register_autograd (
342+ f"{ namespace } ::linear_q4gsw" ,
343+ linear_q4gsw_backward ,
344+ setup_context = linear_q4gsw_setup_context ,
345+ )
346+
292347name = "linear_dq8ca_q4gsw"
293348lib .define (
294349 f"""
@@ -1090,3 +1145,70 @@ def rms_norm_impl(
10901145lib .define (f"{ name } (Tensor x, Tensor weight, float eps) -> Tensor" )
10911146lib .impl (name , rms_norm_impl , "CompositeExplicitAutograd" )
10921147rms_norm_op = getattr (getattr (torch .ops , namespace ), name )
1148+
1149+
1150+ # STE weight gradient d_out^T @ x through the frozen 4-bit linear_q4gsw base.
1151+ def linear_q4gsw_dw_impl (
1152+ d_out : torch .Tensor ,
1153+ x : torch .Tensor ,
1154+ ) -> torch .Tensor :
1155+ return d_out .reshape (- 1 , d_out .shape [- 1 ]).t () @ x .reshape (- 1 , x .shape [- 1 ])
1156+
1157+
1158+ def linear_q4gsw_dw_meta (
1159+ d_out : torch .Tensor ,
1160+ x : torch .Tensor ,
1161+ ) -> torch .Tensor :
1162+ return d_out .new_empty ((d_out .shape [- 1 ], x .shape [- 1 ]))
1163+
1164+
1165+ name = "linear_q4gsw_dw"
1166+ lib .define (f"{ name } (Tensor d_out, Tensor x) -> Tensor" )
1167+ lib .impl (name , linear_q4gsw_dw_impl , "CompositeExplicitAutograd" )
1168+ lib .impl (name , linear_q4gsw_dw_meta , "Meta" )
1169+ linear_q4gsw_dw_op = getattr (getattr (torch .ops , namespace ), name )
1170+
1171+
1172+
1173+ ##################
1174+ ## q4gsw_requant ##
1175+ ##################
1176+
1177+
1178+ # STE re-quant of fp32 latent weights into the frozen-scale 4-bit codes.
1179+ def q4gsw_requant_impl (
1180+ latent : torch .Tensor ,
1181+ scales : torch .Tensor ,
1182+ group_size : int ,
1183+ ) -> torch .Tensor :
1184+ n , k = latent .shape
1185+ group_idx = torch .arange (k , device = latent .device ) // group_size
1186+ scale_full = scales .t ()[:, group_idx ] # [N, K]: scales[k // group_size, n]
1187+ nonzero = scale_full != 0
1188+ safe = torch .where (nonzero , scale_full , torch .ones_like (scale_full ))
1189+ q = torch .round (latent / safe )
1190+ q = torch .where (nonzero , q , torch .zeros_like (q ))
1191+ codes = (torch .clamp (q , - 8 , 7 ).to (torch .int32 ) + 8 ) & 0xF # [N, K] in 0..15
1192+ k_packed = (k + 1 ) // 2
1193+ packed = torch .zeros ((n , k_packed ), dtype = torch .uint8 , device = latent .device )
1194+ packed [:, :] = codes [:, 0 ::2 ].to (torch .uint8 )
1195+ if k > 1 :
1196+ high = codes [:, 1 ::2 ].to (torch .uint8 )
1197+ packed [:, : high .shape [1 ]] |= high << 4
1198+ return packed
1199+
1200+
1201+ def q4gsw_requant_meta (
1202+ latent : torch .Tensor ,
1203+ scales : torch .Tensor ,
1204+ group_size : int ,
1205+ ) -> torch .Tensor :
1206+ n , k = latent .shape
1207+ return latent .new_empty ((n , (k + 1 ) // 2 ), dtype = torch .uint8 )
1208+
1209+
1210+ name = "q4gsw_requant"
1211+ lib .define (f"{ name } (Tensor latent, Tensor scales, int group_size) -> Tensor" )
1212+ lib .impl (name , q4gsw_requant_impl , "CompositeExplicitAutograd" )
1213+ lib .impl (name , q4gsw_requant_meta , "Meta" )
1214+ q4gsw_requant_op = getattr (getattr (torch .ops , namespace ), name )
0 commit comments