Skip to content

Commit 912557c

Browse files
committed
Update
[ghstack-poisoned]
1 parent 07631a3 commit 912557c

12 files changed

Lines changed: 1211 additions & 0 deletions

backends/vulkan/custom_ops_lib.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,61 @@ def linear_dq8ca_q4gsw(
289289
lib.impl(name, linear_q4gsw, "CompositeExplicitAutograd")
290290
linear_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+
292347
name = "linear_dq8ca_q4gsw"
293348
lib.define(
294349
f"""
@@ -1090,3 +1145,70 @@ def rms_norm_impl(
10901145
lib.define(f"{name}(Tensor x, Tensor weight, float eps) -> Tensor")
10911146
lib.impl(name, rms_norm_impl, "CompositeExplicitAutograd")
10921147
rms_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)

backends/vulkan/op_registry.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,15 @@ def register_quantizedlinear_cpp_ops():
462462
)
463463

464464

465+
@update_features(exir_ops.edge.et_vk.linear_q4gsw_backward.default)
466+
def register_linear_q4gsw_backward():
467+
return OpFeatures(
468+
inputs_storage=utils.CONTIGUOUS_ANY,
469+
inputs_dtypes=utils.FP_T,
470+
supports_prepacking=True,
471+
)
472+
473+
465474
@update_features(exir_ops.edge.et_vk.linear_dq8ca_q4gsw.default)
466475
def register_linear_dq8ca_q4gsw():
467476
return OpFeatures(
@@ -1778,6 +1787,25 @@ def register_logical_not():
17781787
)
17791788

17801789

1790+
@update_features(exir_ops.edge.et_vk.linear_q4gsw_dw.default)
1791+
def register_linear_q4gsw_dw():
1792+
return OpFeatures(
1793+
inputs_storage=utils.CONTIGUOUS_ANY,
1794+
inputs_dtypes=utils.FP_T,
1795+
supports_prepacking=True,
1796+
)
1797+
1798+
1799+
1800+
1801+
@update_features(exir_ops.edge.et_vk.q4gsw_requant.default)
1802+
def register_q4gsw_requant():
1803+
return OpFeatures(
1804+
inputs_storage=utils.CONTIGUOUS_ANY,
1805+
inputs_dtypes=utils.FP_T,
1806+
)
1807+
1808+
17811809
#######################
17821810
## Utility functions ##
17831811
#######################

backends/webgpu/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ set(WEBGPU_SRCS
3838
runtime/ops/sdpa/Sdpa.cpp
3939
runtime/ops/select_as_symint/SelectAsSymint.cpp
4040
runtime/ops/quantized_linear/QuantizedLinear.cpp
41+
runtime/ops/quantized_linear/QuantizedLinearBackward.cpp
4142
runtime/ops/mul/BinaryOp.cpp
4243
runtime/ops/embedding_q4gsw/EmbeddingQ4gsw.cpp
4344
runtime/ops/rope/RotaryEmbedding.cpp
@@ -68,6 +69,8 @@ set(WEBGPU_SRCS
6869
runtime/ops/linear/Linear.cpp
6970
runtime/ops/embedding/Embedding.cpp
7071
runtime/ops/logical_not/LogicalNot.cpp
72+
runtime/ops/quantized_linear/QuantizedLinearDw.cpp
73+
runtime/ops/quantized_linear/QuantizedLinearRequant.cpp
7174
)
7275

7376
add_library(webgpu_backend ${WEBGPU_SRCS})
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <executorch/backends/webgpu/runtime/WebGPUGraph.h>
10+
#include <executorch/backends/webgpu/runtime/WebGPUUtils.h>
11+
#include <executorch/backends/webgpu/runtime/ops/OperatorRegistry.h>
12+
#include <executorch/backends/webgpu/runtime/ops/quantized_linear/q4gsw_backward_wgsl.h>
13+
14+
#include <webgpu/webgpu.h>
15+
16+
#include <cstdint>
17+
#include <cstring>
18+
#include <stdexcept>
19+
20+
namespace executorch::backends::webgpu {
21+
22+
namespace {
23+
24+
struct Q4gswBackwardParams {
25+
uint32_t M;
26+
uint32_t N;
27+
uint32_t K;
28+
uint32_t K_packed;
29+
uint32_t group_size;
30+
uint32_t padded_N;
31+
uint32_t has_bias;
32+
uint32_t _pad;
33+
};
34+
static_assert(sizeof(Q4gswBackwardParams) == 32, "params must be 32 bytes");
35+
36+
// linear_q4gsw_backward: d_x[M,K] = d_out[M,N] @ dequant(W)[N,K].
37+
void q4gsw_backward_impl(WebGPUGraph& graph, const std::vector<int>& args) {
38+
const int dout_id = args.at(0);
39+
const int weight_id = args.at(1);
40+
const int scales_id = args.at(2);
41+
const int group_size_id = args.at(3);
42+
const int dx_id = args.at(4);
43+
44+
WGPUDevice device = graph.device();
45+
const auto& dout = graph.get_tensor(dout_id);
46+
const auto& weight = graph.get_tensor(weight_id);
47+
const auto& scales = graph.get_tensor(scales_id);
48+
const auto& dx = graph.get_tensor(dx_id);
49+
50+
if (weight.dims.size() != 2 || scales.dims.size() != 2 || dx.dims.empty() ||
51+
dout.dims.empty()) {
52+
throw std::runtime_error("q4gsw_backward: bad tensor ranks");
53+
}
54+
const uint32_t N = static_cast<uint32_t>(weight.dims[0]);
55+
const uint32_t K_packed = static_cast<uint32_t>(weight.dims[1]);
56+
const uint32_t K = static_cast<uint32_t>(dx.dims.back());
57+
if (N == 0 || K == 0) {
58+
throw std::runtime_error("q4gsw_backward: N or K == 0");
59+
}
60+
uint64_t dx_numel = 1;
61+
for (int64_t d : dx.dims) {
62+
dx_numel *= static_cast<uint64_t>(d);
63+
}
64+
const uint32_t M = static_cast<uint32_t>(dx_numel / K);
65+
const uint32_t num_groups = static_cast<uint32_t>(scales.dims[0]);
66+
const uint32_t padded_N = static_cast<uint32_t>(scales.dims[1]);
67+
68+
if (graph.get_value_type(group_size_id) != WebGPUGraph::ValueType::Int) {
69+
throw std::runtime_error("q4gsw_backward: group_size must be Int");
70+
}
71+
const int64_t group_size = graph.get_int(group_size_id);
72+
if (group_size <= 0) {
73+
throw std::runtime_error("q4gsw_backward: group_size must be positive");
74+
}
75+
const uint32_t gs = static_cast<uint32_t>(group_size);
76+
77+
// fp32 + shape guards (mirror the forward's byte checks).
78+
if (dx.nbytes != dx_numel * sizeof(float)) {
79+
throw std::runtime_error("q4gsw_backward: d_x fp32-only");
80+
}
81+
if (dout.nbytes != static_cast<uint64_t>(M) * N * sizeof(float)) {
82+
throw std::runtime_error("q4gsw_backward: d_out fp32/shape mismatch");
83+
}
84+
if (scales.nbytes != static_cast<uint64_t>(num_groups) * padded_N * sizeof(float)) {
85+
throw std::runtime_error("q4gsw_backward: scales fp32/shape mismatch");
86+
}
87+
if (weight.nbytes != static_cast<uint64_t>(N) * K_packed) {
88+
throw std::runtime_error("q4gsw_backward: weight byte-size mismatch");
89+
}
90+
if (K_packed != (K + 1u) / 2u) {
91+
throw std::runtime_error("q4gsw_backward: K_packed != ceil(K/2)");
92+
}
93+
if (num_groups < (K + gs - 1u) / gs || padded_N < N) {
94+
throw std::runtime_error("q4gsw_backward: scales too small");
95+
}
96+
97+
Q4gswBackwardParams params = {};
98+
params.M = M;
99+
params.N = N;
100+
params.K = K;
101+
params.K_packed = K_packed;
102+
params.group_size = gs;
103+
params.padded_N = padded_N;
104+
105+
const uint32_t wg_size =
106+
utils::clamp_workgroup_size(device, kQ4gswBackwardWorkgroupSizeX);
107+
const uint64_t tiles = static_cast<uint64_t>((M + 3u) / 4u) * ((K + 3u) / 4u);
108+
if (tiles > UINT32_MAX) {
109+
throw std::runtime_error("q4gsw_backward: tile count exceeds u32");
110+
}
111+
const uint32_t workgroup_count = utils::compute_1d_workgroup_count(
112+
device, static_cast<uint32_t>(tiles), wg_size, "q4gsw_backward");
113+
114+
WGPUBufferDescriptor uniform_desc = {};
115+
uniform_desc.size = sizeof(Q4gswBackwardParams);
116+
uniform_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
117+
uniform_desc.mappedAtCreation = true;
118+
WGPUBuffer uniform_buffer = wgpuDeviceCreateBuffer(device, &uniform_desc);
119+
std::memcpy(
120+
wgpuBufferGetMappedRange(uniform_buffer, 0, sizeof(Q4gswBackwardParams)),
121+
&params,
122+
sizeof(Q4gswBackwardParams));
123+
wgpuBufferUnmap(uniform_buffer);
124+
graph.add_uniform_buffer_bytes(sizeof(Q4gswBackwardParams));
125+
126+
WGPUShaderSourceWGSL wgsl_desc = {};
127+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
128+
wgsl_desc.code = {kQ4gswBackwardWGSL, WGPU_STRLEN};
129+
WGPUShaderModuleDescriptor shader_desc = {};
130+
shader_desc.nextInChain = &wgsl_desc.chain;
131+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
132+
133+
WGPUBindGroupLayoutEntry entries[5] = {};
134+
entries[0].binding = 0;
135+
entries[0].visibility = WGPUShaderStage_Compute;
136+
entries[0].buffer.type = WGPUBufferBindingType_Storage;
137+
for (uint32_t i = 1; i <= 3; i++) {
138+
entries[i].binding = i;
139+
entries[i].visibility = WGPUShaderStage_Compute;
140+
entries[i].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
141+
}
142+
entries[4].binding = 4;
143+
entries[4].visibility = WGPUShaderStage_Compute;
144+
entries[4].buffer.type = WGPUBufferBindingType_Uniform;
145+
146+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
147+
bgl_desc.entryCount = 5;
148+
bgl_desc.entries = entries;
149+
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
150+
151+
WGPUPipelineLayoutDescriptor pl_desc = {};
152+
pl_desc.bindGroupLayoutCount = 1;
153+
pl_desc.bindGroupLayouts = &bgl;
154+
WGPUPipelineLayout pipeline_layout =
155+
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
156+
157+
WGPUConstantEntry wg_size_constant = {};
158+
wg_size_constant.key = {"wg_size", WGPU_STRLEN};
159+
wg_size_constant.value = static_cast<double>(wg_size);
160+
161+
WGPUComputePipelineDescriptor pipeline_desc = {};
162+
pipeline_desc.layout = pipeline_layout;
163+
pipeline_desc.compute.module = shader;
164+
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
165+
pipeline_desc.compute.constantCount = 1;
166+
pipeline_desc.compute.constants = &wg_size_constant;
167+
WGPUComputePipeline pipeline =
168+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
169+
170+
WGPUBindGroupEntry bg_entries[5] = {};
171+
bg_entries[0].binding = 0;
172+
bg_entries[0].buffer = dx.buffer;
173+
bg_entries[0].size = dx.nbytes;
174+
bg_entries[1].binding = 1;
175+
bg_entries[1].buffer = dout.buffer;
176+
bg_entries[1].size = dout.nbytes;
177+
bg_entries[2].binding = 2;
178+
bg_entries[2].buffer = weight.buffer;
179+
bg_entries[2].size = weight.nbytes;
180+
bg_entries[3].binding = 3;
181+
bg_entries[3].buffer = scales.buffer;
182+
bg_entries[3].size = scales.nbytes;
183+
bg_entries[4].binding = 4;
184+
bg_entries[4].buffer = uniform_buffer;
185+
bg_entries[4].size = sizeof(Q4gswBackwardParams);
186+
187+
WGPUBindGroupDescriptor bg_desc = {};
188+
bg_desc.layout = bgl;
189+
bg_desc.entryCount = 5;
190+
bg_desc.entries = bg_entries;
191+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
192+
193+
graph.add_dispatch({pipeline, bind_group, workgroup_count, "q4gsw_backward"});
194+
195+
wgpuShaderModuleRelease(shader);
196+
wgpuBindGroupLayoutRelease(bgl);
197+
wgpuPipelineLayoutRelease(pipeline_layout);
198+
wgpuBufferRelease(uniform_buffer);
199+
}
200+
201+
} // namespace
202+
203+
WEBGPU_REGISTER_OPERATORS {
204+
WEBGPU_REGISTER_OP(et_vk.linear_q4gsw_backward.default, q4gsw_backward_impl);
205+
}
206+
207+
} // namespace executorch::backends::webgpu

0 commit comments

Comments
 (0)