Skip to content

Commit 249e39f

Browse files
committed
[ExecuTorch][Vulkan] Add et_vk.linear_q4gsw_backward kernel (4-bit input-grad)
Pull Request resolved: #20943 **Adds the Vulkan `et_vk.linear_q4gsw_backward` kernel** — the input-gradient of the frozen 4-bit `linear_q4gsw` base, for on-device adapter training. Computes `d_x[M, K] = d_out[M, N] @ dequant(W)[N, K]`, contracting over N. **Problem:** `et_vk.linear_q4gsw_backward` is registered in the shared Vulkan partitioner (`custom_ops_lib.py` + `op_registry.py`) but Vulkan had no runtime kernel, so the op could not run. **Solution:** a 4M x 4K register-tiled GLSL kernel that reads the SAME W_4X8 block-packed weight the forward reads, re-deriving the nibble/scale addressing so the training loop stays consistent with the forward with no re-pack. `dequant(W[n, k]) = (code - 8) * scale`. Key changes: - `glsl/q4gsw_backward.{glsl,yaml}` — buffer x float; W_4X8 nibble unpack mirroring `glsl/q4gsw_linear_gemm__w_4x8.glsl` (even-N low nibble, odd-N high, `N4_padded` ivec4 stride, `[num_groups, N]` scales). - `impl/QuantizedLinearBackward.cpp` — reuses `prepack_q4_w_4x8_nc_buffer` + `prepack_q4_scales` from the forward; 1D tile dispatch `ceil(M/4) * ceil(K/4)` with a workgroup-count guard; `group_size` specialization constant. **Constraints:** buffer storage, fp32; `N % 4 == 0`, `K % 4 == 0`, `group_size % 4 == 0` (matches the forward prepack). Weight/scale layout identical to the forward. ghstack-source-id: 405059111 @exported-using-ghexport Differential Revision: [D111797529](https://our.internmc.facebook.com/intern/diff/D111797529/)
1 parent c0e09e0 commit 249e39f

3 files changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
#version 450 core
10+
11+
#define PRECISION ${PRECISION}
12+
13+
#define T ${texel_load_component_type(DTYPE, STORAGE)}
14+
15+
#define TILE_M 4
16+
#define TILE_K 4
17+
18+
${define_required_extensions(STORAGE, DTYPE)}
19+
${define_required_extensions("buffer", DTYPE)}
20+
21+
layout(std430) buffer;
22+
23+
${layout_declare_tensor(B, "w", "t_dx", DTYPE, STORAGE, is_scalar_array=True)}
24+
${layout_declare_tensor(B, "r", "t_dout", DTYPE, STORAGE, is_scalar_array=True)}
25+
${layout_declare_tensor(B, "r", "t_q4_weights", "int", "buffer", is_scalar_array=False, vec_size=4)}
26+
${layout_declare_tensor(B, "r", "t_scales", DTYPE, "buffer", is_scalar_array=True)}
27+
28+
${layout_declare_ubo(B, "ivec4", "dout_sizes")}
29+
${layout_declare_ubo(B, "ivec4", "dx_sizes")}
30+
31+
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;
32+
33+
${layout_declare_spec_const(C, "int", "group_size", "32")}
34+
35+
// d_x[M, K] = d_out[M, N] @ dequant(W)[N, K], contracting over N.
36+
// dequant(W[n, k]) = (code - 8) * scale, with code read from the same W_4X8
37+
// block-packed weight the forward reads (mirrors q4gsw_linear_gemm__w_4x8.glsl).
38+
void main() {
39+
const int N = dout_sizes.x;
40+
const int M = dout_sizes.y * dout_sizes.z * dout_sizes.w;
41+
const int K = dx_sizes.x;
42+
43+
const int nmt = (M + TILE_M - 1) / TILE_M;
44+
const int nkt = (K + TILE_K - 1) / TILE_K;
45+
const int tiles = nmt * nkt;
46+
47+
const int tile_idx = int(gl_GlobalInvocationID.x);
48+
if (tile_idx >= tiles) {
49+
return;
50+
}
51+
52+
const int m0 = (tile_idx / nkt) * TILE_M;
53+
const int k0 = (tile_idx % nkt) * TILE_K;
54+
55+
// K and N are multiples of 4 (prepack guarantees), so k0 is 4-aligned: the
56+
// tile's 4 K lanes are byte b = kl of one k4 group and share one scale group.
57+
const int k4 = k0 >> 2;
58+
const int N4 = (N + 3) >> 2;
59+
const int N4_padded = (N4 + 1) & ~1;
60+
const int N8 = N4_padded >> 1;
61+
const int group = k0 / group_size;
62+
63+
float acc[TILE_M * TILE_K];
64+
for (int i = 0; i < TILE_M * TILE_K; ++i) {
65+
acc[i] = 0.0;
66+
}
67+
68+
for (int n = 0; n < N; ++n) {
69+
float dout_reg[TILE_M];
70+
for (int ml = 0; ml < TILE_M; ++ml) {
71+
const int m_eff = min(m0 + ml, M - 1);
72+
dout_reg[ml] = float(t_dout[m_eff * N + n]);
73+
}
74+
75+
// W_4X8 address for column n: ivec4 at (k4, n8); component by (n4 parity,
76+
// n-in-tile half); low/high nibble by n parity (even-N low, odd-N high).
77+
const int n4 = n >> 2;
78+
const int ni = n & 3;
79+
const int n8 = n4 >> 1;
80+
const int comp = (n4 & 1) * 2 + (ni >> 1);
81+
const int nib_hi = (ni & 1) * 4;
82+
const ivec4 w_block = t_q4_weights[k4 * N8 + n8];
83+
const int w_int = w_block[comp];
84+
const float scale = float(t_scales[group * N + n]);
85+
86+
for (int kl = 0; kl < TILE_K; ++kl) {
87+
const int code = int((uint(w_int) >> (8 * kl + nib_hi)) & 0xFu);
88+
const float dq = float(code - 8) * scale;
89+
for (int ml = 0; ml < TILE_M; ++ml) {
90+
acc[ml * TILE_K + kl] += dout_reg[ml] * dq;
91+
}
92+
}
93+
}
94+
95+
for (int ml = 0; ml < TILE_M; ++ml) {
96+
const int m = m0 + ml;
97+
for (int kl = 0; kl < TILE_K; ++kl) {
98+
const int k = k0 + kl;
99+
if (m < M && k < K) {
100+
t_dx[m * K + k] = T(acc[ml * TILE_K + kl]);
101+
}
102+
}
103+
}
104+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
q4gsw_backward:
8+
parameter_names_with_default_values:
9+
DTYPE: float
10+
STORAGE: buffer
11+
generate_variant_forall:
12+
STORAGE:
13+
- VALUE: buffer
14+
DTYPE:
15+
- VALUE: float
16+
shader_variants:
17+
- NAME: q4gsw_backward
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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/vulkan/runtime/graph/ops/OperatorRegistry.h>
10+
11+
#include <executorch/backends/vulkan/runtime/graph/ops/DynamicDispatchNode.h>
12+
13+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Common.h>
14+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Q4gswLinear.h>
15+
16+
#include <executorch/backends/vulkan/runtime/graph/ops/utils/ShaderNameUtils.h>
17+
18+
namespace vkcompute {
19+
20+
// Resize d_x to d_out.shape[:-1] + (K,), mirroring linear_q4gsw_backward_meta.
21+
// extra_args = { weight_data, d_out }.
22+
void resize_linear_q4gsw_backward_node(
23+
ComputeGraph* graph,
24+
const std::vector<ArgGroup>& args,
25+
const std::vector<ValueRef>& extra_args) {
26+
const ValueRef d_x = args.at(0).refs.at(0);
27+
const ValueRef weight_data = extra_args.at(0);
28+
const ValueRef d_out = extra_args.at(1);
29+
const int64_t K = graph->sizes_of(weight_data).at(1) * 2;
30+
std::vector<int64_t> new_sizes = graph->sizes_of(d_out);
31+
new_sizes.back() = K;
32+
graph->virtual_resize(d_x, new_sizes);
33+
}
34+
35+
utils::uvec3 linear_q4gsw_backward_global_wg_size(
36+
ComputeGraph* graph,
37+
const vkapi::ShaderInfo& shader,
38+
const std::vector<ArgGroup>& args,
39+
const std::vector<ValueRef>& resize_args) {
40+
(void)shader;
41+
(void)resize_args;
42+
const ValueRef d_x = args.at(0).refs.at(0);
43+
const uint32_t K = graph->size_at<uint32_t>(-1, d_x);
44+
const uint32_t M = utils::safe_downcast<uint32_t>(graph->numel_of(d_x) / K);
45+
const uint32_t tiles = utils::div_up_4(M) * utils::div_up_4(K);
46+
return {tiles, 1u, 1u};
47+
}
48+
49+
utils::uvec3 linear_q4gsw_backward_local_wg_size(
50+
ComputeGraph* graph,
51+
const vkapi::ShaderInfo& shader,
52+
const utils::uvec3& global_workgroup_size,
53+
const std::vector<ArgGroup>& args,
54+
const std::vector<ValueRef>& resize_args) {
55+
(void)graph;
56+
(void)shader;
57+
(void)global_workgroup_size;
58+
(void)args;
59+
(void)resize_args;
60+
return {64u, 1u, 1u};
61+
}
62+
63+
void linear_q4gsw_backward(
64+
ComputeGraph& graph,
65+
const std::vector<ValueRef>& args) {
66+
int32_t i = 0;
67+
const ValueRef d_out = args.at(i++);
68+
const ValueRef weight_data = args.at(i++);
69+
const ValueRef weight_scales_data = args.at(i++);
70+
const ValueRef group_size_ref = args.at(i++);
71+
const ValueRef d_x = args.at(i++);
72+
73+
VK_CHECK_COND(graph.dtype_of(d_out) == vkapi::kFloat);
74+
VK_CHECK_COND(graph.dtype_of(d_x) == vkapi::kFloat);
75+
VK_CHECK_COND(graph.is_buffer_storage(d_out));
76+
VK_CHECK_COND(graph.is_buffer_storage(d_x));
77+
78+
const vkapi::ScalarType in_dtype = graph.dtype_of(d_out);
79+
const int64_t group_size_val = graph.extract_scalar<int64_t>(group_size_ref);
80+
VK_CHECK_COND(group_size_val > 0 && group_size_val % 4 == 0);
81+
82+
const std::vector<int64_t> weight_sizes = graph.sizes_of(weight_data);
83+
const int64_t N = weight_sizes.at(0);
84+
const int64_t K = weight_sizes.at(1) * 2;
85+
VK_CHECK_COND(N > 0 && K > 0);
86+
VK_CHECK_COND(N % 4 == 0 && K % 4 == 0);
87+
VK_CHECK_COND(K % group_size_val == 0);
88+
VK_CHECK_COND(graph.size_at<int64_t>(-1, d_out) == N);
89+
90+
const ValueRef packed_weight = prepack_q4_w_4x8_nc_buffer(graph, weight_data);
91+
const ValueRef packed_scales =
92+
prepack_q4_scales(graph, weight_scales_data, in_dtype);
93+
94+
const uint32_t M = utils::safe_downcast<uint32_t>(graph.numel_of(d_out) / N);
95+
const uint32_t tiles =
96+
utils::div_up_4(M) * utils::div_up_4(static_cast<uint32_t>(K));
97+
VK_CHECK_COND(
98+
(tiles + 63u) / 64u <= 65535u,
99+
"linear_q4gsw_backward: tile count exceeds max workgroup count");
100+
101+
std::string kernel_name = "q4gsw_backward";
102+
kernel_name.reserve(kShaderNameReserve);
103+
add_storage_type_suffix(kernel_name, graph.storage_type_of(d_x));
104+
add_dtype_suffix(kernel_name, graph.dtype_of(d_x));
105+
106+
graph.execute_nodes().emplace_back(new DynamicDispatchNode(
107+
graph,
108+
VK_KERNEL_FROM_STR(kernel_name),
109+
linear_q4gsw_backward_global_wg_size,
110+
linear_q4gsw_backward_local_wg_size,
111+
// Inputs and Outputs
112+
{{d_x, vkapi::kWrite},
113+
{{d_out, packed_weight, packed_scales}, vkapi::kRead}},
114+
// Shader params buffers
115+
{graph.sizes_ubo(d_out), graph.sizes_ubo(d_x)},
116+
// Push Constants
117+
{},
118+
// Specialization Constants
119+
{static_cast<uint32_t>(group_size_val)},
120+
// Resize Args
121+
{weight_data, d_out},
122+
// Resizing Logic
123+
resize_linear_q4gsw_backward_node));
124+
}
125+
126+
REGISTER_OPERATORS {
127+
VK_REGISTER_OP(et_vk.linear_q4gsw_backward.default, linear_q4gsw_backward);
128+
}
129+
130+
} // namespace vkcompute

0 commit comments

Comments
 (0)