Skip to content

Commit 4a2c7be

Browse files
committed
[ExecuTorch][Vulkan] Tests for et_vk.q4gsw_requant
Pull Request resolved: #20946 **Correctness tests for the Vulkan `et_vk.q4gsw_requant` kernel** (stacked above the op diff). **Coverage:** the golden codes are computed with ATen (`round`/`clamp`, zero-scale -> code 8), mirroring `quant_nibble`, then packed into the expected W_4X8 int buffer with a small bit-packing reference (data-reshaping only, no hand-rolled math). The kernel output is compared int-for-int against that buffer, which locks the exact byte layout the forward reads. The latent is built as `code * scale` so `round()` is unambiguous (no `.5` tie-break divergence). Cases: - `test_tile_aligned` — single group, tile-aligned N/K. - `test_grouped` — multiple quantization groups along K. - `test_odd_n4` — `N % 8 != 0` (odd N4 -> padded stride + bias-zero OOB tile). - `test_zero_scale` — a zero scale must yield code 8, not a divide-by-zero. Also wires `q4gsw_requant_test` into `targets.bzl` + `CMakeLists.txt`. ghstack-source-id: 405059122 @exported-using-ghexport Differential Revision: [D111797526](https://our.internmc.facebook.com/intern/diff/D111797526/)
1 parent 05481a6 commit 4a2c7be

3 files changed

Lines changed: 205 additions & 0 deletions

File tree

backends/vulkan/test/op_tests/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ if(TARGET vulkan_backend AND LIB_TORCH)
129129
quantized_linear_backward_test
130130
${CMAKE_CURRENT_SOURCE_DIR}/quantized_linear_backward_test.cpp test_utils
131131
)
132+
vulkan_op_test(
133+
q4gsw_requant_test ${CMAKE_CURRENT_SOURCE_DIR}/q4gsw_requant_test.cpp
134+
test_utils
135+
)
132136

133137
# Only build generated op tests if a path to tags.yaml and
134138
# native_functions.yaml is provided. These files are required for codegen.
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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 <gtest/gtest.h>
10+
11+
#include <ATen/ATen.h>
12+
13+
#include <executorch/backends/vulkan/runtime/api/api.h>
14+
#include <executorch/backends/vulkan/runtime/graph/ComputeGraph.h>
15+
#include <executorch/backends/vulkan/runtime/graph/ops/OperatorRegistry.h>
16+
17+
#include "test_utils.h"
18+
19+
//
20+
// Reference Implementation
21+
//
22+
23+
// Pack [N, K] codes (0..15) into the W_4X8 block-packed int buffer the forward
24+
// reads. Mirrors pack_q4_linear_weight__w_4x8.glsl: one ivec4 per (k4, n8),
25+
// byte b holds an (even-N low nibble, odd-N high nibble) pair at K = k4*4 + b.
26+
std::vector<int32_t> pack_codes_w4x8(const at::Tensor& codes) {
27+
const int64_t N = codes.size(0);
28+
const int64_t K = codes.size(1);
29+
const int64_t K4 = K / 4;
30+
const int64_t N4 = N / 4;
31+
const int64_t N4_padded = (N4 + 1) & ~int64_t{1};
32+
const int64_t N8 = N4_padded / 2;
33+
std::vector<int32_t> buf(K4 * N4_padded * 2, 0);
34+
auto ca = codes.accessor<int, 2>();
35+
36+
auto pack_tile = [&](int64_t k4, int64_t n4, uint32_t& px, uint32_t& py) {
37+
px = 0u;
38+
py = 0u;
39+
for (int ni = 0; ni < 4; ++ni) {
40+
const int64_t n = n4 * 4 + ni;
41+
for (int b = 0; b < 4; ++b) {
42+
const uint32_t code = static_cast<uint32_t>(ca[n][k4 * 4 + b] & 0xF);
43+
const int shift = 8 * b + (ni & 1) * 4;
44+
if (ni < 2) {
45+
px |= code << shift;
46+
} else {
47+
py |= code << shift;
48+
}
49+
}
50+
}
51+
};
52+
53+
for (int64_t k4 = 0; k4 < K4; ++k4) {
54+
for (int64_t n8 = 0; n8 < N8; ++n8) {
55+
const int64_t n4_a = 2 * n8;
56+
const int64_t n4_b = n4_a + 1;
57+
uint32_t px_a, py_a, px_b = 0x88888888u, py_b = 0x88888888u;
58+
pack_tile(k4, n4_a, px_a, py_a);
59+
if (n4_b < N4) {
60+
pack_tile(k4, n4_b, px_b, py_b);
61+
}
62+
const int64_t base = (k4 * N8 + n8) * 4;
63+
buf[base + 0] = static_cast<int32_t>(px_a);
64+
buf[base + 1] = static_cast<int32_t>(py_a);
65+
buf[base + 2] = static_cast<int32_t>(px_b);
66+
buf[base + 3] = static_cast<int32_t>(py_b);
67+
}
68+
}
69+
return buf;
70+
}
71+
72+
//
73+
// Test function
74+
//
75+
76+
void test_vulkan_q4gsw_requant_impl(
77+
const int64_t N,
78+
const int64_t K,
79+
const int64_t group_size,
80+
const bool with_zero_scale) {
81+
const int64_t num_groups = K / group_size;
82+
83+
at::Tensor scales =
84+
at::rand({num_groups, N}, at::device(at::kCPU).dtype(at::kFloat)) + 0.5;
85+
if (with_zero_scale) {
86+
scales.index_put_({0, 0}, 0.0);
87+
}
88+
89+
const at::Tensor group_idx =
90+
at::arange(K, at::device(at::kCPU).dtype(at::kLong))
91+
.div(group_size, "floor");
92+
const at::Tensor scale_full =
93+
scales.t().contiguous().index_select(1, group_idx); // [N, K]
94+
95+
// Deterministic quotient targets, each >=0.2 from any .5 tie, so GPU fp32
96+
// division (~2.5 ULP, not correctly rounded) and the CPU golden round
97+
// identically. Covers round both directions and clamp past [-8, 7].
98+
const std::vector<float> pattern = {
99+
0.3f, -0.4f, 2.7f, -3.3f, 6.4f, -6.4f, 13.2f, -21.7f};
100+
const at::Tensor pat =
101+
at::tensor(pattern, at::device(at::kCPU).dtype(at::kFloat));
102+
const at::Tensor q_idx =
103+
at::arange(N * K, at::device(at::kCPU).dtype(at::kLong))
104+
.remainder(static_cast<int64_t>(pattern.size()));
105+
const at::Tensor target_q = pat.index_select(0, q_idx).reshape({N, K});
106+
at::Tensor latent = target_q * scale_full;
107+
108+
// Golden codes, mirroring quant_nibble: q=0 where scale==0, else roundEven
109+
// (matches at::round half-to-even); clamp to [-8, 7]; code = (q + 8) & 0xF.
110+
const at::Tensor nonzero = scale_full != 0;
111+
const at::Tensor safe =
112+
at::where(nonzero, scale_full, at::ones_like(scale_full));
113+
at::Tensor q = at::round(latent / safe);
114+
q = at::where(nonzero, q, at::zeros_like(q));
115+
q = at::clamp(q, -8, 7);
116+
const at::Tensor golden_codes =
117+
(q.to(at::kInt) + 8).bitwise_and(0xF); // [N, K] in 0..15
118+
119+
const std::vector<int32_t> expected = pack_codes_w4x8(golden_codes);
120+
121+
using namespace vkcompute;
122+
123+
GraphConfig config;
124+
ComputeGraph graph(config);
125+
126+
IOValueRef r_latent = graph.add_input_tensor(
127+
latent.sizes().vec(),
128+
from_at_scalartype(latent.scalar_type()),
129+
utils::kBuffer);
130+
ValueRef r_scales = graph.add_tensorref(
131+
scales.sizes().vec(),
132+
from_at_scalartype(scales.scalar_type()),
133+
scales.const_data_ptr());
134+
const ValueRef r_group_size = graph.add_scalar<int64_t>(group_size);
135+
136+
const int64_t N4 = N / 4;
137+
const int64_t N4_padded = (N4 + 1) & ~int64_t{1};
138+
const ValueRef r_packed =
139+
graph.add_tensor({(K / 4) * N4_padded * 2}, vkapi::kInt, utils::kBuffer);
140+
141+
VK_GET_OP_FN("et_vk.q4gsw_requant.default")
142+
(graph, {r_latent.value, r_scales, r_group_size, r_packed});
143+
144+
ValueRef staging_out = graph.set_output_tensor(r_packed);
145+
146+
graph.prepare();
147+
graph.prepack();
148+
graph.propagate_resize();
149+
150+
graph.maybe_cast_and_copy_into_staging(
151+
r_latent.staging,
152+
latent.const_data_ptr(),
153+
latent.numel(),
154+
from_at_scalartype(latent.scalar_type()));
155+
156+
graph.execute();
157+
158+
at::Tensor vk_packed = at::empty(
159+
{static_cast<int64_t>(expected.size())},
160+
at::device(at::kCPU).dtype(at::kInt));
161+
graph.maybe_cast_and_copy_from_staging(
162+
staging_out,
163+
vk_packed.mutable_data_ptr(),
164+
vk_packed.numel(),
165+
from_at_scalartype(vk_packed.scalar_type()));
166+
167+
auto va = vk_packed.accessor<int, 1>();
168+
for (size_t i = 0; i < expected.size(); ++i) {
169+
ASSERT_EQ(va[i], expected[i]) << "mismatch at packed int " << i;
170+
}
171+
}
172+
173+
// Tile-aligned single-group.
174+
TEST(VulkanQ4gswRequantTest, test_tile_aligned) {
175+
test_vulkan_q4gsw_requant_impl(
176+
/*N=*/16, /*K=*/32, /*group_size=*/32, /*with_zero_scale=*/false);
177+
}
178+
179+
// Multiple quantization groups along K.
180+
TEST(VulkanQ4gswRequantTest, test_grouped) {
181+
test_vulkan_q4gsw_requant_impl(
182+
/*N=*/32, /*K=*/64, /*group_size=*/32, /*with_zero_scale=*/false);
183+
}
184+
185+
// N not a multiple of 8 (odd N4 -> padded stride + bias-zero OOB tile).
186+
TEST(VulkanQ4gswRequantTest, test_odd_n4) {
187+
test_vulkan_q4gsw_requant_impl(
188+
/*N=*/12, /*K=*/16, /*group_size=*/16, /*with_zero_scale=*/false);
189+
}
190+
191+
// A zero scale must produce the bias-zero code (8), not a divide-by-zero.
192+
TEST(VulkanQ4gswRequantTest, test_zero_scale) {
193+
test_vulkan_q4gsw_requant_impl(
194+
/*N=*/16, /*K=*/32, /*group_size=*/32, /*with_zero_scale=*/true);
195+
}

backends/vulkan/test/op_tests/targets.bzl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ def define_common_targets(is_fbcode = False):
204204
":test_utils",
205205
]
206206
)
207+
define_test_targets(
208+
"q4gsw_requant_test",
209+
extra_deps = [
210+
":test_utils",
211+
]
212+
)
207213
define_test_targets(
208214
"rms_norm_test",
209215
extra_deps = [

0 commit comments

Comments
 (0)