Skip to content

Commit a30109f

Browse files
committed
[ExecuTorch][Vulkan] Add et_vk.q4gsw_requant kernel (STE re-quant to W_4X8)
Pull Request resolved: #20945 **Adds the Vulkan `et_vk.q4gsw_requant` kernel** — straight-through re-quant of fp32 latent weights into the frozen-scale 4-bit codes, for on-device weight training. Writes the codes directly in the forward's W_4X8 layout so no per-step re-pack is needed. **Problem:** `et_vk.q4gsw_requant` is registered in the shared Vulkan partitioner but Vulkan had no runtime kernel. **Solution:** a GLSL kernel that quantizes `round(latent / scale)` clamped to `[-8, 7]` and packs the codes in the W_4X8 block layout the forward reads, reusing the exact byte-pair convention of `glsl/pack_q4_linear_weight__w_4x8.glsl` (even-N low nibble, odd-N high; one ivec4 per 4K x 8N block at `(k4, n8)`; OOB upper tile = the bias-zero `0x88888888`). A zero scale yields code 8 (no divide-by-zero), matching the eager reference. Key changes: - `glsl/q4gsw_requant.{glsl,yaml}` — buffer x float; 2D dispatch over `(k4, n8)`. - `impl/QuantizedLinearRequant.cpp` — prepacks the constant scales; output is the W_4X8 int buffer `[K4 * N4_padded * 2]`; `group_size` spec constant; dispatch-grid guard. **Design note (for review):** per the layout decision, requant writes W_4X8 to match the Vulkan forward. Today the forward and backward each prepack their own flat `[N, K/2]` weight internally, so nothing yet consumes an externally-produced W_4X8 buffer — closing the training loop needs a forward path that reads a mutable pre-packed weight (the weight-lifecycle follow-up). The op's AOT meta in `custom_ops_lib.py` still describes the flat `[N, K/2]` output and should be reconciled with this W_4X8 output. **Constraints:** buffer storage, fp32 latent/scales; `N % 4 == 0`, `K % 4 == 0`, `group_size % 4 == 0`. ghstack-source-id: 405059118 @exported-using-ghexport Differential Revision: [D111797527](https://our.internmc.facebook.com/intern/diff/D111797527/)
1 parent 9fccbdd commit a30109f

3 files changed

Lines changed: 245 additions & 0 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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_required_extensions(STORAGE, DTYPE)}
14+
15+
layout(std430) buffer;
16+
17+
${layout_declare_tensor(B, "w", "t_packed", "int", "buffer", is_scalar_array=False, vec_size=4)}
18+
${layout_declare_tensor(B, "r", "t_latent", DTYPE, STORAGE, is_scalar_array=True)}
19+
${layout_declare_tensor(B, "r", "t_scales", DTYPE, "buffer", is_scalar_array=True)}
20+
21+
${layout_declare_ubo(B, "ivec4", "latent_sizes")}
22+
23+
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;
24+
25+
${layout_declare_spec_const(C, "int", "group_size", "32")}
26+
27+
// STE re-quant of fp32 latent [N, K] into the W_4X8 block-packed 4-bit codes
28+
// the forward reads (mirrors pack_q4_linear_weight__w_4x8.glsl). Each thread
29+
// writes one 4K x 8N block (an ivec4) at (k4, n8).
30+
uint quant_nibble(const int n, const int k, const int N, const int K) {
31+
const float s = float(t_scales[(k / group_size) * N + n]);
32+
// roundEven + clamp-in-float match torch.round (half-to-even) then clamp.
33+
float qf = 0.0;
34+
if (s != 0.0) {
35+
qf = clamp(roundEven(float(t_latent[n * K + k]) / s), -8.0, 7.0);
36+
}
37+
return uint(int(qf) + 8) & 0xFu;
38+
}
39+
40+
// Pack a 4K x 4N tile into the (packed_x, packed_y) int pair. Byte b holds one
41+
// (even-N low nibble, odd-N high nibble) pair at K = k4*4 + b; rows N0,N1 go to
42+
// packed_x, rows N2,N3 to packed_y.
43+
void pack_tile(
44+
out uint packed_x,
45+
out uint packed_y,
46+
const int k4,
47+
const int n4,
48+
const int N,
49+
const int K) {
50+
packed_x = 0u;
51+
packed_y = 0u;
52+
for (int ni = 0; ni < 4; ++ni) {
53+
const int n = n4 * 4 + ni;
54+
for (int b = 0; b < 4; ++b) {
55+
const uint code = quant_nibble(n, k4 * 4 + b, N, K);
56+
const int shift = 8 * b + (ni & 1) * 4;
57+
if (ni < 2) {
58+
packed_x |= code << shift;
59+
} else {
60+
packed_y |= code << shift;
61+
}
62+
}
63+
}
64+
}
65+
66+
void main() {
67+
const int k4 = int(gl_GlobalInvocationID.x);
68+
const int n8 = int(gl_GlobalInvocationID.y);
69+
70+
const int K = latent_sizes.x;
71+
const int N = latent_sizes.y;
72+
const int K4 = K >> 2;
73+
const int N4 = (N + 3) >> 2;
74+
const int N8 = (N4 + 1) >> 1;
75+
76+
if (k4 >= K4 || n8 >= N8) {
77+
return;
78+
}
79+
80+
const int n4_a = 2 * n8;
81+
const int n4_b = n4_a + 1;
82+
83+
uint packed_x_a = 0u;
84+
uint packed_y_a = 0u;
85+
// OOB upper tile (odd N4 boundary) is the bias-zero pattern, matching the
86+
// forward's prepack padding so the whole block is always readable.
87+
uint packed_x_b = 0x88888888u;
88+
uint packed_y_b = 0x88888888u;
89+
90+
pack_tile(packed_x_a, packed_y_a, k4, n4_a, N, K);
91+
if (n4_b < N4) {
92+
pack_tile(packed_x_b, packed_y_b, k4, n4_b, N, K);
93+
}
94+
95+
t_packed[k4 * N8 + n8] = ivec4(
96+
int(packed_x_a), int(packed_y_a), int(packed_x_b), int(packed_y_b));
97+
}
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_requant:
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_requant
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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/Staging.h>
15+
16+
#include <executorch/backends/vulkan/runtime/graph/ops/utils/ShaderNameUtils.h>
17+
18+
namespace vkcompute {
19+
20+
// Resize the packed output to the W_4X8 int buffer size K4 * N4_padded * 2,
21+
// matching prepack_q4_w_4x8_nc_buffer's layout. extra_args = { latent }.
22+
void resize_q4gsw_requant_node(
23+
ComputeGraph* graph,
24+
const std::vector<ArgGroup>& args,
25+
const std::vector<ValueRef>& extra_args) {
26+
const ValueRef packed = args.at(0).refs.at(0);
27+
const ValueRef latent = extra_args.at(0);
28+
const std::vector<int64_t> latent_sizes = graph->sizes_of(latent);
29+
const int64_t N = latent_sizes.at(0);
30+
const int64_t K = latent_sizes.at(1);
31+
const int64_t K4 = K / 4;
32+
const int64_t N4 = N / 4;
33+
const int64_t N4_padded = (N4 + 1) & ~int64_t{1};
34+
graph->virtual_resize(packed, {K4 * N4_padded * 2});
35+
}
36+
37+
utils::uvec3 q4gsw_requant_global_wg_size(
38+
ComputeGraph* graph,
39+
const vkapi::ShaderInfo& shader,
40+
const std::vector<ArgGroup>& args,
41+
const std::vector<ValueRef>& resize_args) {
42+
(void)shader;
43+
(void)resize_args;
44+
const ValueRef latent = args.at(1).refs.at(0);
45+
const std::vector<int64_t> latent_sizes = graph->sizes_of(latent);
46+
const uint32_t N = utils::safe_downcast<uint32_t>(latent_sizes.at(0));
47+
const uint32_t K = utils::safe_downcast<uint32_t>(latent_sizes.at(1));
48+
const uint32_t K4 = K / 4u;
49+
const uint32_t N4 = (N + 3u) / 4u;
50+
const uint32_t N8 = (N4 + 1u) / 2u;
51+
return {K4, N8, 1u};
52+
}
53+
54+
utils::uvec3 q4gsw_requant_local_wg_size(
55+
ComputeGraph* graph,
56+
const vkapi::ShaderInfo& shader,
57+
const utils::uvec3& global_workgroup_size,
58+
const std::vector<ArgGroup>& args,
59+
const std::vector<ValueRef>& resize_args) {
60+
(void)graph;
61+
(void)shader;
62+
(void)global_workgroup_size;
63+
(void)args;
64+
(void)resize_args;
65+
return {8u, 8u, 1u};
66+
}
67+
68+
void q4gsw_requant(ComputeGraph& graph, const std::vector<ValueRef>& args) {
69+
int32_t i = 0;
70+
const ValueRef latent = args.at(i++);
71+
const ValueRef scales = args.at(i++);
72+
const ValueRef group_size_ref = args.at(i++);
73+
const ValueRef packed = args.at(i++);
74+
75+
VK_CHECK_COND(graph.dtype_of(latent) == vkapi::kFloat);
76+
VK_CHECK_COND(graph.dtype_of(scales) == vkapi::kFloat);
77+
VK_CHECK_COND(graph.dtype_of(packed) == vkapi::kInt);
78+
VK_CHECK_COND(graph.is_buffer_storage(latent));
79+
VK_CHECK_COND(graph.is_buffer_storage(packed));
80+
81+
const int64_t group_size_val = graph.extract_scalar<int64_t>(group_size_ref);
82+
VK_CHECK_COND(group_size_val > 0 && group_size_val % 4 == 0);
83+
84+
const std::vector<int64_t> latent_sizes = graph.sizes_of(latent);
85+
VK_CHECK_COND(latent_sizes.size() == 2);
86+
const int64_t N = latent_sizes.at(0);
87+
const int64_t K = latent_sizes.at(1);
88+
VK_CHECK_COND(N > 0 && K > 0);
89+
VK_CHECK_COND(N % 4 == 0 && K % 4 == 0);
90+
VK_CHECK_COND(K % group_size_val == 0);
91+
92+
const uint32_t K4 = utils::safe_downcast<uint32_t>(K / 4);
93+
const uint32_t N4 = utils::safe_downcast<uint32_t>(N / 4);
94+
const uint32_t N8 = (N4 + 1u) / 2u;
95+
VK_CHECK_COND(
96+
K4 <= 65535u && N8 <= 65535u,
97+
"q4gsw_requant: dispatch grid exceeds max workgroup count");
98+
99+
// Scales are a frozen constant; materialize them to a GPU buffer once.
100+
const ValueRef packed_scales =
101+
prepack_standard(graph, scales, utils::kBuffer, utils::kWidthPacked);
102+
103+
std::string kernel_name = "q4gsw_requant";
104+
kernel_name.reserve(kShaderNameReserve);
105+
add_storage_type_suffix(kernel_name, graph.storage_type_of(latent));
106+
add_dtype_suffix(kernel_name, graph.dtype_of(latent));
107+
108+
graph.execute_nodes().emplace_back(new DynamicDispatchNode(
109+
graph,
110+
VK_KERNEL_FROM_STR(kernel_name),
111+
q4gsw_requant_global_wg_size,
112+
q4gsw_requant_local_wg_size,
113+
// Inputs and Outputs
114+
{{packed, vkapi::kWrite}, {{latent, packed_scales}, vkapi::kRead}},
115+
// Shader params buffers
116+
{graph.sizes_ubo(latent)},
117+
// Push Constants
118+
{},
119+
// Specialization Constants
120+
{static_cast<uint32_t>(group_size_val)},
121+
// Resize Args
122+
{latent},
123+
// Resizing Logic
124+
resize_q4gsw_requant_node));
125+
}
126+
127+
REGISTER_OPERATORS {
128+
VK_REGISTER_OP(et_vk.q4gsw_requant.default, q4gsw_requant);
129+
}
130+
131+
} // namespace vkcompute

0 commit comments

Comments
 (0)