-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathtest_awq_gptq_dequant.cpp
More file actions
279 lines (247 loc) · 11.4 KB
/
Copy pathtest_awq_gptq_dequant.cpp
File metadata and controls
279 lines (247 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// Ports the executable spec of vLLM's AWQ/GPTQ INT4 dequant:
// tests/kernels/quantization/test_awq_triton.py::awq_dequantize_torch @ 555967922
// (the reference (iweights - zeros) * scales with reverse-AWQ order)
// csrc/libtorch_stable/quantization/gptq/qdq_4.cuh dequant_4bit_8_gptq +
// q_gemm.cu:201-202 zero_offset (GPTQv1 vs v2) @ 555967922
//
// Two-layer gate (mirrors test_nvfp4_dequant.cpp): (1) hand-computed known
// packed int32s with exact-integer expected bf16 give an INDEPENDENT arithmetic
// oracle and pin the bit order / zero convention; (2) a randomized roundtrip
// packs nibbles through an INDEPENDENT reference packer (mirroring the vLLM
// layout) and checks the production unpack+dequant against a DOUBLE-precision
// reference, so any packing-axis / group-index / order bug surfaces.
#include <doctest/doctest.h>
#include <cstdint>
#include <random>
#include <vector>
#include "vllm/model_executor/model_loader/awq_gptq_dequant.h"
#include "vt/dtype.h"
using vllm::DequantAwq4ToBf16;
using vllm::DequantGptq4ToBf16;
using vllm::kAwqReverseOrder;
namespace {
// --- Independent reference packers (mirror the vLLM on-disk layout) ---
// AWQ weight/zero packer: [rows][N] nibbles -> int32 [rows][N/8], packed along
// N with reverse-AWQ order (auto_awq.py:77). Output element `col` lands at
// nibble kAwqReverseOrder[col%8] of int32 col/8.
std::vector<int32_t> PackAwqAlongN(const std::vector<std::vector<int>>& nib,
int64_t rows, int64_t n) {
std::vector<int32_t> out(rows * (n / 8), 0);
for (int64_t r = 0; r < rows; ++r) {
for (int64_t c = 0; c < n; ++c) {
const int64_t pc = c / 8;
const int shift = kAwqReverseOrder[c % 8] * 4;
out[r * (n / 8) + pc] |=
static_cast<int32_t>((nib[r][c] & 0xF) << shift);
}
}
return out;
}
// GPTQ weight packer: [K][N] nibbles -> int32 [K/8][N], packed along K with
// standard order (quant_utils.gptq_pack == pack_rows). Row k lands at nibble
// k%8 of int32 row k/8.
std::vector<int32_t> PackGptqWeightAlongK(const std::vector<std::vector<int>>& nib,
int64_t k, int64_t n) {
std::vector<int32_t> out((k / 8) * n, 0);
for (int64_t row = 0; row < k; ++row) {
for (int64_t c = 0; c < n; ++c) {
const int shift = static_cast<int>(row % 8) * 4;
out[(row / 8) * n + c] |=
static_cast<int32_t>((nib[row][c] & 0xF) << shift);
}
}
return out;
}
// GPTQ zero packer: [G][N] nibbles -> int32 [G][N/8], packed along N with
// STANDARD order. Column n lands at nibble n%8 of int32 n/8.
std::vector<int32_t> PackGptqZerosAlongN(const std::vector<std::vector<int>>& nib,
int64_t groups, int64_t n) {
std::vector<int32_t> out(groups * (n / 8), 0);
for (int64_t g = 0; g < groups; ++g) {
for (int64_t c = 0; c < n; ++c) {
const int shift = static_cast<int>(c % 8) * 4;
out[g * (n / 8) + c / 8] |=
static_cast<int32_t>((nib[g][c] & 0xF) << shift);
}
}
return out;
}
} // namespace
// --- AWQ hand-computed: one row, one 8-wide group. Independent-arithmetic
// oracle for the reverse-AWQ order and (w - z) * s. col7 = 15 forces a set MSB
// on the packed int32 so a sign-extension unpack bug would surface. ---
TEST_CASE("DequantAwq4ToBf16 hand-computed reverse order + sign safety") {
// desired output cols: 5,3,0,15,8,1,2,15
// reverse[e]: e->pos {0:0,1:4,2:1,3:5,4:2,5:6,6:3,7:7}
// pos0=col0=5 pos1=col2=0 pos2=col4=8 pos3=col6=2
// pos4=col1=3 pos5=col3=15 pos6=col5=1 pos7=col7=15 => 0xF1F32805
std::vector<int32_t> qweight = {static_cast<int32_t>(0xF1F32805)};
std::vector<int32_t> qzeros = {static_cast<int32_t>(0x22222222)}; // z=2 all cols
std::vector<float> scales = {1.0F, 2.0F, 4.0F, 1.0F, 1.0F, 1.0F, 1.0F, 1.0F};
std::vector<uint16_t> out(8, 0xFFFF);
DequantAwq4ToBf16(qweight.data(), scales.data(), qzeros.data(), /*k=*/1,
/*n=*/8, /*group_size=*/1, out.data());
const float expected[8] = {
(5 - 2) * 1.0F, // 3
(3 - 2) * 2.0F, // 2
(0 - 2) * 4.0F, // -8
(15 - 2) * 1.0F, // 13
(8 - 2) * 1.0F, // 6
(1 - 2) * 1.0F, // -1
(2 - 2) * 1.0F, // 0
(15 - 2) * 1.0F, // 13
};
for (int i = 0; i < 8; ++i) {
CHECK(vt::BF16ToF32(out[i]) == doctest::Approx(expected[i]));
}
}
// --- GPTQ hand-computed: 8 K-rows x 8 cols (N%8==0, qzeros packed along N),
// standard K-packing; column 0 is the meaningful one (rows 1..7 all zero).
// Verifies the zero_offset convention for BOTH v1 (offset 1) and v2 (offset 0). ---
TEST_CASE("DequantGptq4ToBf16 hand-computed zero_offset v1/v2") {
// column 0 rows k0..k7 = 5,3,0,15,8,1,2,7 -> standard K-pack = 0x7218F035;
// columns 1..7 all zero. qweight is [K/8=1][N=8].
std::vector<int32_t> qweight(8, 0);
qweight[0] = static_cast<int32_t>(0x7218F035);
// qzeros [num_groups=1][N/8=1]: col0 zero at standard nibble 0 = 3, cols 1..7 = 0.
std::vector<int32_t> qzeros = {0x00000003};
std::vector<float> scales(8, 1.0F); // [1][8]
const int rows[8] = {5, 3, 0, 15, 8, 1, 2, 7};
SUBCASE("v1 (zero_offset=1): eff_zero = 3 + 1 = 4") {
std::vector<uint16_t> out(8 * 8, 0xFFFF);
DequantGptq4ToBf16(qweight.data(), scales.data(), qzeros.data(),
/*g_idx=*/nullptr, /*k=*/8, /*n=*/8, /*group_size=*/8,
/*zero_offset=*/1, out.data());
for (int k = 0; k < 8; ++k) {
CHECK(vt::BF16ToF32(out[k * 8 + 0]) ==
doctest::Approx(static_cast<float>(rows[k] - 4)));
}
}
SUBCASE("v2 (zero_offset=0): eff_zero = 3") {
std::vector<uint16_t> out(8 * 8, 0xFFFF);
DequantGptq4ToBf16(qweight.data(), scales.data(), qzeros.data(),
/*g_idx=*/nullptr, /*k=*/8, /*n=*/8, /*group_size=*/8,
/*zero_offset=*/0, out.data());
for (int k = 0; k < 8; ++k) {
CHECK(vt::BF16ToF32(out[k * 8 + 0]) ==
doctest::Approx(static_cast<float>(rows[k] - 3)));
}
}
}
// --- GPTQ act-order: g_idx remaps rows to groups so a row-stride/group-index
// bug surfaces. K=8, N=8, 2 groups; g_idx sends even rows to group 0, odd to
// group 1, each group with a distinct zero, and confirms scale/zero indexing
// follows g_idx not k/group_size. ---
TEST_CASE("DequantGptq4ToBf16 act-order g_idx group selection") {
const int64_t k = 8, n = 8, groups = 2;
std::vector<std::vector<int>> wn(k, std::vector<int>(n, 0));
for (int64_t r = 0; r < k; ++r)
for (int64_t c = 0; c < n; ++c) wn[r][c] = static_cast<int>((r + c) % 16);
auto qweight = PackGptqWeightAlongK(wn, k, n);
std::vector<std::vector<int>> zn(groups, std::vector<int>(n, 0));
for (int64_t c = 0; c < n; ++c) {
zn[0][c] = 1; // group 0 zero
zn[1][c] = 4; // group 1 zero
}
auto qzeros = PackGptqZerosAlongN(zn, groups, n);
std::vector<float> scales(groups * n);
for (int64_t g = 0; g < groups; ++g)
for (int64_t c = 0; c < n; ++c) scales[g * n + c] = (g == 0) ? 1.0F : 2.0F;
// even rows -> group 0, odd rows -> group 1 (a permutation groupsize=1 could
// never produce).
std::vector<int32_t> g_idx(k);
for (int64_t r = 0; r < k; ++r) g_idx[r] = static_cast<int32_t>(r % 2);
std::vector<uint16_t> out(k * n, 0xFFFF);
DequantGptq4ToBf16(qweight.data(), scales.data(), qzeros.data(), g_idx.data(),
k, n, /*group_size=*/1, /*zero_offset=*/0, out.data());
for (int64_t r = 0; r < k; ++r) {
const int64_t g = r % 2;
const int zval = (g == 0) ? 1 : 4;
const float s = (g == 0) ? 1.0F : 2.0F;
for (int64_t c = 0; c < n; ++c) {
const float exp = (static_cast<float>(wn[r][c]) - zval) * s;
CHECK(vt::BF16ToF32(out[r * n + c]) == doctest::Approx(exp));
}
}
}
// --- Randomized roundtrip vs a DOUBLE-precision reference. Nibbles/zeros/scales
// are drawn at random, packed through the independent reference packers, and the
// production dequant is compared to a double-precision (w - z) * s. Exercises
// multi-group row/col offset arithmetic that the hand cases do not. ---
TEST_CASE("DequantAwq4ToBf16 randomized roundtrip vs double reference") {
std::mt19937 rng(0xA0B1C2D3U);
std::uniform_int_distribution<int> nib(0, 15);
std::uniform_real_distribution<float> sdist(-2.0F, 2.0F);
const int64_t k = 64, n = 32, group_size = 16;
const int64_t groups = k / group_size;
std::vector<std::vector<int>> wn(k, std::vector<int>(n));
for (auto& row : wn)
for (auto& v : row) v = nib(rng);
std::vector<std::vector<int>> zn(groups, std::vector<int>(n));
for (auto& row : zn)
for (auto& v : row) v = nib(rng);
std::vector<float> scales(groups * n);
for (auto& s : scales) s = sdist(rng);
auto qweight = PackAwqAlongN(wn, k, n);
auto qzeros = PackAwqAlongN(zn, groups, n);
std::vector<uint16_t> out(k * n, 0xFFFF);
DequantAwq4ToBf16(qweight.data(), scales.data(), qzeros.data(), k, n,
group_size, out.data());
for (int64_t r = 0; r < k; ++r) {
const int64_t g = r / group_size;
for (int64_t c = 0; c < n; ++c) {
const double ref =
(static_cast<double>(wn[r][c]) - zn[g][c]) *
static_cast<double>(scales[g * n + c]);
// bf16 has ~8 mantissa bits: independent double ref within bf16 rounding.
CHECK(vt::BF16ToF32(out[r * n + c]) ==
doctest::Approx(ref).epsilon(0.01));
}
}
}
TEST_CASE("DequantGptq4ToBf16 randomized roundtrip vs double reference") {
std::mt19937 rng(0x1234ABCDU);
std::uniform_int_distribution<int> nib(0, 15);
std::uniform_real_distribution<float> sdist(-2.0F, 2.0F);
const int64_t k = 64, n = 32, group_size = 16;
const int64_t groups = k / group_size;
const int zero_offset = 1; // classic AutoGPTQ
std::vector<std::vector<int>> wn(k, std::vector<int>(n));
for (auto& row : wn)
for (auto& v : row) v = nib(rng);
std::vector<std::vector<int>> zn(groups, std::vector<int>(n));
for (auto& row : zn)
for (auto& v : row) v = nib(rng);
std::vector<float> scales(groups * n);
for (auto& s : scales) s = sdist(rng);
auto qweight = PackGptqWeightAlongK(wn, k, n);
auto qzeros = PackGptqZerosAlongN(zn, groups, n);
std::vector<uint16_t> out(k * n, 0xFFFF);
DequantGptq4ToBf16(qweight.data(), scales.data(), qzeros.data(),
/*g_idx=*/nullptr, k, n, group_size, zero_offset,
out.data());
for (int64_t r = 0; r < k; ++r) {
const int64_t g = r / group_size;
for (int64_t c = 0; c < n; ++c) {
const double ref =
(static_cast<double>(wn[r][c]) - (zn[g][c] + zero_offset)) *
static_cast<double>(scales[g * n + c]);
CHECK(vt::BF16ToF32(out[r * n + c]) ==
doctest::Approx(ref).epsilon(0.01));
}
}
}
// --- Guard rails: null / mis-sized inputs abort. ---
TEST_CASE("DequantAwq4/Gptq4 argument validation") {
std::vector<int32_t> qw = {0};
std::vector<int32_t> qz = {0};
std::vector<float> sc(8, 1.0F);
std::vector<uint16_t> out(8, 0);
CHECK_THROWS(DequantAwq4ToBf16(nullptr, sc.data(), qz.data(), 1, 8, 1, out.data()));
CHECK_THROWS(DequantAwq4ToBf16(qw.data(), sc.data(), qz.data(), 1, 7, 1, out.data())); // n%8
CHECK_THROWS(DequantAwq4ToBf16(qw.data(), sc.data(), qz.data(), 1, 8, 3, out.data())); // G!|k
CHECK_THROWS(DequantGptq4ToBf16(qw.data(), sc.data(), qz.data(), nullptr, 4, 8, 4, 1,
out.data())); // k%8
CHECK_THROWS(DequantGptq4ToBf16(qw.data(), sc.data(), qz.data(), nullptr, 8, 8, 8, 2,
out.data())); // zero_offset
}