|
| 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 | +} |
0 commit comments