Skip to content

Commit 630ddba

Browse files
authored
[cuda backend] int4/8 matvec: vectorized activation load (pytorch#20144)
The decode-only int4_plain_mm matvec was bound by activation load-instruction throughput, not DRAM bandwidth (already ~64% peak) or latency. Each inner iteration issued ~15 loads per 16-byte weight chunk: 8 scalar int32 activation loads + the same per-block scale d reloaded 4x. Same as int8_plain_mm Align Q8Block to 16 bytes (sizeof 36->48) so each block's qs_even/qs_odd 16B halves are 16B-aligned, then load a whole activation block with two vectorized uint4 loads + one d load (~4x fewer activation loads). dp4a math and accumulation order are bit-identical; the int8 activation values and scale are unchanged. gemma4_31b decode (long-ctx harness, stacked on optimize_1): decode 43.98 -> 46.557 tok/s (+6.4%), +12.7% compare with llama.cpp (41.5 token/s) profile result: int4 matvec avg 38.4 -> 34.75 us (-9.5%); quant kernel unchanged.
1 parent 7282106 commit 630ddba

2 files changed

Lines changed: 38 additions & 23 deletions

File tree

backends/cuda/runtime/shims/int4_plain_mm.cuh

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ __host__ __forceinline__ int32_t log2_pow2(int32_t v) {
5555
// blocks)
5656
// ---------------------------------------------------------------------------
5757

58-
struct Q8Block {
58+
// alignas(16) pads sizeof(Q8Block) to 48 so each block (and its qs_even/qs_odd
59+
// 16-byte halves) is 16-byte aligned. This lets the matvec load a whole block's
60+
// int8 activations with two vectorized uint4 loads instead of eight scalar
61+
// int32 loads, cutting activation load instructions ~4x.
62+
struct alignas(16) Q8Block {
5963
int8_t qs_even[Q8_BLOCK_SIZE / 2];
6064
int8_t qs_odd[Q8_BLOCK_SIZE / 2];
6165
float d; // scale
@@ -149,6 +153,18 @@ __global__ void __launch_bounds__(MV_THREADS)
149153
int32_t k_base = i * 32;
150154
uint32_t words[4] = {packed16.x, packed16.y, packed16.z, packed16.w};
151155

156+
// One uint4 (32 weights) maps to exactly one Q8 activation block (32
157+
// activations), i.e. q8_block_idx == i. Load the whole block with two
158+
// vectorized uint4 loads (+ one scale load) instead of eight scalar int32
159+
// loads. ae.{x,y,z,w} == qs_even[0:4],[4:8],[8:12],[12:16] == a_even for
160+
// w=0..3 (same for ao/qs_odd) -> bit-identical to the scalar path.
161+
const Q8Block* qb = &q8_row[i];
162+
uint4 ae = *reinterpret_cast<const uint4*>(qb->qs_even);
163+
uint4 ao = *reinterpret_cast<const uint4*>(qb->qs_odd);
164+
float a_scale = qb->d;
165+
const uint32_t a_even[4] = {ae.x, ae.y, ae.z, ae.w};
166+
const uint32_t a_odd[4] = {ao.x, ao.y, ao.z, ao.w};
167+
152168
#pragma unroll
153169
for (int32_t w = 0; w < 4; w++) {
154170
uint32_t packed = words[w];
@@ -164,22 +180,11 @@ __global__ void __launch_bounds__(MV_THREADS)
164180
int32_t vi_lo = packed & 0x0F0F0F0F;
165181
int32_t vi_hi = (packed >> 4) & 0x0F0F0F0F;
166182

167-
int32_t q8_block_idx = k_word / Q8_BLOCK_SIZE;
168-
int32_t q8_half_offset = (k_word % Q8_BLOCK_SIZE) / 2;
169-
const Q8Block* qb = &q8_row[q8_block_idx];
170-
171-
int32_t a_even =
172-
*reinterpret_cast<const int32_t*>(qb->qs_even + q8_half_offset);
173-
int32_t a_odd =
174-
*reinterpret_cast<const int32_t*>(qb->qs_odd + q8_half_offset);
175-
176-
int32_t dp = __dp4a(vi_lo, a_even, 0);
177-
dp = __dp4a(vi_hi, a_odd, dp);
178-
179-
float a_scale = qb->d;
183+
int32_t dp = __dp4a(vi_lo, static_cast<int32_t>(a_even[w]), 0);
184+
dp = __dp4a(vi_hi, static_cast<int32_t>(a_odd[w]), dp);
180185

181-
int32_t a_sum8 = __dp4a(0x01010101, a_even, 0);
182-
a_sum8 = __dp4a(0x01010101, a_odd, a_sum8);
186+
int32_t a_sum8 = __dp4a(0x01010101, static_cast<int32_t>(a_even[w]), 0);
187+
a_sum8 = __dp4a(0x01010101, static_cast<int32_t>(a_odd[w]), a_sum8);
183188

184189
sum += ws * a_scale *
185190
(static_cast<float>(dp) - wz * static_cast<float>(a_sum8));

backends/cuda/runtime/shims/int8_plain_mm.cuh

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ __host__ __forceinline__ int32_t log2_pow2_i8(int32_t v) {
5858
// blocks, NATURAL order — qs[k] holds the quantized value for element k).
5959
// ---------------------------------------------------------------------------
6060

61-
struct Q8BlockNat {
61+
// alignas(16) pads sizeof(Q8BlockNat) 36->48 so each block (and its two 16-byte
62+
// qs halves) is 16-byte aligned. This lets the matvec load 16 int8 activations
63+
// with one vectorized uint4 load instead of four scalar int32 loads, cutting
64+
// activation load instructions ~4x.
65+
struct alignas(16) Q8BlockNat {
6266
int8_t qs[Q8_NAT_BLOCK_SIZE];
6367
float d; // scale
6468
};
@@ -135,6 +139,17 @@ __global__ void __launch_bounds__(MV8_THREADS) int8_w8a8_matvec_kernel(
135139
int32_t k_base = i * 16;
136140
uint32_t words[4] = {packed16.x, packed16.y, packed16.z, packed16.w};
137141

142+
// One uint4 (16 int8 weights) maps to exactly one 16-byte half of a Q8
143+
// activation block (16 activations): block i>>1, byte offset 0 (i even) or
144+
// 16 (i odd). Load those 16 int8 activations with a single vectorized uint4
145+
// load (+ one scale load) instead of four scalar int32 loads + four scale
146+
// reloads. av.{x,y,z,w} == qs[off+0:4],[4:8],[8:12],[12:16] == a_word for
147+
// w=0..3 -> bit-identical to the scalar path.
148+
const Q8BlockNat* qb = &q8_row[i >> 1];
149+
uint4 av = *reinterpret_cast<const uint4*>(qb->qs + ((i & 1) ? 16 : 0));
150+
float a_scale = qb->d;
151+
const uint32_t a_words[4] = {av.x, av.y, av.z, av.w};
152+
138153
#pragma unroll
139154
for (int32_t w = 0; w < 4; w++) {
140155
int32_t k_word = k_base + w * 4; // 4 int8 weights start here
@@ -147,15 +162,10 @@ __global__ void __launch_bounds__(MV8_THREADS) int8_w8a8_matvec_kernel(
147162
}
148163

149164
int32_t w_word = static_cast<int32_t>(words[w]);
150-
151-
int32_t q8_block_idx = k_word / Q8_NAT_BLOCK_SIZE;
152-
int32_t q8_offset = k_word % Q8_NAT_BLOCK_SIZE;
153-
const Q8BlockNat* qb = &q8_row[q8_block_idx];
154-
int32_t a_word = *reinterpret_cast<const int32_t*>(qb->qs + q8_offset);
165+
int32_t a_word = static_cast<int32_t>(a_words[w]);
155166

156167
int32_t dp = __dp4a(w_word, a_word, 0);
157168
int32_t a_sum = __dp4a(0x01010101, a_word, 0);
158-
float a_scale = qb->d;
159169

160170
sum += ws * a_scale *
161171
(static_cast<float>(dp) - wz * static_cast<float>(a_sum));

0 commit comments

Comments
 (0)