Spaces:
Sleeping
Sleeping
File size: 17,784 Bytes
28a1a01 40d0cd2 28a1a01 | 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 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | // AVX-VNNI / AVX512-VNNI integer-MAC path (Zen4/Zen5, Alder Lake+, SPR).
//
// One kernel: int8 weights x int8 activations (the VNNI-native 8-bit ABI)
// with per-32 fp32 dequant scales, selected only when the CPU has the exact
// encoding this TU was built for (VEX dpbusd on GCC -mavxvnni, EVEX dpbusd
// on MSVC /arch:AVX512). Pre-VNNI machines never execute this file: the
// int16 Q8 AVX2 path (kernels_avx2.cpp) stays their fast path and scalar
// stays the fallback.
//
// Math (per 32-block, integer-exact): dpbusd needs (u8, s8), weights are
// s8, so bias weights by +128 (xor 0x80) and correct:
// sum(w*a) = dpbusd(w^0x80, a) - 128*sum(a).
// int32 is exact here (|sum| <= 32*127*127 = 516k, |lane| <= ~131k), so any
// horizontal-sum order is bit-identical and the fused single-hsum form below
// equals the old two-hsum form lane-for-lane (checked in numpy over 20k
// random blocks; test_dpbusd_bias_correction_math covers the identity).
// Float accumulation uses 4 independent chains like the AVX2 Q8 kernels;
// order differs from AVX2 (scalar mul+add vs FMA), so numeric changes stay
// PPL-gated (int8 PPL gate holds at +0.3% on the AVX2 reference; VNNI
// hardware CI must re-confirm, see VALIDATION.md). Weight row scales stay on
// the caller side, mirroring dot_int8_q8_avx2.
//
// AVX2-standard audit (against dot_int8_q8_avx2 in kernels_avx2.cpp):
// 128-wide outer loop (4x32, 4 float chains in fixed order) + 64-wide
// (2x32, sequential adds, bitwise-identical to two 32-iterations) + 32-wide
// + scalar tail matches the Q8-family blocking; the 64-wide step is the Q8
// analogue of the fp32 kernels' 64-wide inner blocking (halves tail-loop
// overhead, no float-order change). T0 prefetches (+256 elements on both
// int8 streams, 2 iterations ahead) mirror dot_int8_q8_avx2 exactly (its
// int16 act prefetch covers twice the bytes for the same 2-iteration lead);
// x86 prefetches never fault, skipped when n < 128. The partial tail reads
// only to the row's logical end with act_scales[i/32].
//
// Provenance / performance note: the integer math is proven in numpy on any
// CPU; the intrinsic mapping itself is VNNI-hardware-CI-pending (this box is
// Zen3, no VNNI). Single-token decode stays bandwidth-bound (weights stream
// from DRAM either way), so the dpbusd win concentrates in prefill and
// spec-verify GEMM work (tokens resident, weights reused from cache).
#include "kernels.hpp"
#include <cstddef>
#include <cstdint>
#if defined(CISM_HAVE_AVX_VNNI) || defined(CISM_HAVE_AVX512_VNNI)
#include <immintrin.h>
#endif
namespace cism {
#if defined(CISM_HAVE_AVX_VNNI) || defined(CISM_HAVE_AVX512_VNNI)
namespace {
// Register-only horizontal sum of 8 int32 lanes (deterministic, no
// store-forwarding stall). Reordering vs a stored-lanes sum is bit-identical:
// int32 never overflows here, so addition is associative.
inline int hsum_epi32(__m256i v) {
__m128i s = _mm_add_epi32(_mm256_castsi256_si128(v), _mm256_extracti128_si256(v, 1));
s = _mm_add_epi32(s, _mm_shuffle_epi32(s, _MM_SHUFFLE(1, 0, 3, 2)));
s = _mm_add_epi32(s, _mm_shuffle_epi32(s, _MM_SHUFFLE(0, 0, 0, 1)));
return _mm_cvtsi128_si32(s);
}
// Exact integer block dot for 32 int8 pairs via one dpbusd + correction.
// bias/ones are hoisted by the caller (loop-invariant broadcasts). sum(a) is
// derived from the already-loaded act register (extract halves, no act
// reloads) and the -128*sum(a) correction is fused into the dp lanes
// (slli x128 + sub) so a single hsum finishes the block. A psadbw-based
// sum(a) was considered and rejected: it needs an extra act xor for the same
// single hsum, saving nothing after this fusion.
inline int block_dot32_vnni(const std::int8_t* weights, const std::int8_t* act,
__m256i bias, __m256i ones) {
const __m256i w = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(weights));
const __m256i a = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(act));
const __m256i dp = _mm256_dpbusd_epi32(
_mm256_setzero_si256(), _mm256_xor_si256(w, bias), a);
const __m256i s = _mm256_add_epi32(
_mm256_madd_epi16(_mm256_cvtepi8_epi16(_mm256_castsi256_si128(a)), ones),
_mm256_madd_epi16(_mm256_cvtepi8_epi16(_mm256_extracti128_si256(a, 1)), ones));
return hsum_epi32(_mm256_sub_epi32(dp, _mm256_slli_epi32(s, 7)));
}
} // namespace
// AVX512 zmm fp32 dot (needs only AVX512F; gated on VNNI presence like the
// rest of this TU to keep one dispatch story). 8 accumulators over 128-wide
// blocks mirror the AVX2 kernel's structure; chunking (128/64/32/16) and the
// final horizontal+scalar reduce differ, so results match AVX2 within normal
// fp32 rounding (PPL-gated, NOT bitwise-identical — validate on VNNI tin).
// VL-free reduce (split to 256-bit halves) so GCC needs no extra -mavx512vl.
static float reduce_sum512(__m512 value) {
const __m256 added = _mm256_add_ps(_mm512_castps512_ps256(value),
_mm256_castpd_ps(_mm512_extractf64x4_pd(_mm512_castps_pd(value), 1)));
__m128 sum = _mm_add_ps(_mm256_castps256_ps128(added), _mm256_extractf128_ps(added, 1));
sum = _mm_add_ps(sum, _mm_movehl_ps(sum, sum));
sum = _mm_add_ss(sum, _mm_shuffle_ps(sum, sum, _MM_SHUFFLE(1, 1, 1, 1)));
return _mm_cvtss_f32(sum);
}
float dot_fp32_avx512(const float* a, const float* b, std::size_t n) {
__m512 sum0 = _mm512_setzero_ps(), sum1 = _mm512_setzero_ps();
__m512 sum2 = _mm512_setzero_ps(), sum3 = _mm512_setzero_ps();
__m512 sum4 = _mm512_setzero_ps(), sum5 = _mm512_setzero_ps();
__m512 sum6 = _mm512_setzero_ps(), sum7 = _mm512_setzero_ps();
std::size_t i = 0;
for (; i + 128 <= n; i += 128, a += 128, b += 128) {
_mm_prefetch(reinterpret_cast<const char*>(a + 64), _MM_HINT_T0);
_mm_prefetch(reinterpret_cast<const char*>(b + 64), _MM_HINT_T0);
sum0 = _mm512_fmadd_ps(_mm512_loadu_ps(a), _mm512_loadu_ps(b), sum0);
sum1 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 16), _mm512_loadu_ps(b + 16), sum1);
sum2 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 32), _mm512_loadu_ps(b + 32), sum2);
sum3 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 48), _mm512_loadu_ps(b + 48), sum3);
sum4 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 64), _mm512_loadu_ps(b + 64), sum4);
sum5 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 80), _mm512_loadu_ps(b + 80), sum5);
sum6 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 96), _mm512_loadu_ps(b + 96), sum6);
sum7 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 112), _mm512_loadu_ps(b + 112), sum7);
}
for (; i + 64 <= n; i += 64, a += 64, b += 64) {
sum0 = _mm512_fmadd_ps(_mm512_loadu_ps(a), _mm512_loadu_ps(b), sum0);
sum1 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 16), _mm512_loadu_ps(b + 16), sum1);
sum2 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 32), _mm512_loadu_ps(b + 32), sum2);
sum3 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 48), _mm512_loadu_ps(b + 48), sum3);
}
for (; i + 32 <= n; i += 32, a += 32, b += 32) {
sum0 = _mm512_fmadd_ps(_mm512_loadu_ps(a), _mm512_loadu_ps(b), sum0);
sum1 = _mm512_fmadd_ps(_mm512_loadu_ps(a + 16), _mm512_loadu_ps(b + 16), sum1);
}
for (; i + 16 <= n; i += 16, a += 16, b += 16)
sum0 = _mm512_fmadd_ps(_mm512_loadu_ps(a), _mm512_loadu_ps(b), sum0);
__m512 sum = _mm512_add_ps(_mm512_add_ps(_mm512_add_ps(sum0, sum1), _mm512_add_ps(sum2, sum3)),
_mm512_add_ps(_mm512_add_ps(sum4, sum5), _mm512_add_ps(sum6, sum7)));
float result = reduce_sum512(sum);
for (; i < n; ++i, ++a, ++b) result += *a * *b;
return result;
}
// AVX512 zmm int8 dot (fp32 activations): 64-byte loads + widen + FMA at 2x
// the AVX2 width, 8 accumulators, masked <16 tail (maskz loads cannot fault).
// Same per-element math as dot_int8_avx2; chunking/reduce differ (PPL-gated).
float dot_int8_avx512(const std::int8_t* weights, const float* input, std::size_t n) {
__m512 sum0 = _mm512_setzero_ps(), sum1 = _mm512_setzero_ps();
__m512 sum2 = _mm512_setzero_ps(), sum3 = _mm512_setzero_ps();
__m512 sum4 = _mm512_setzero_ps(), sum5 = _mm512_setzero_ps();
__m512 sum6 = _mm512_setzero_ps(), sum7 = _mm512_setzero_ps();
std::size_t i = 0;
for (; i + 128 <= n; i += 128, weights += 128, input += 128) {
_mm_prefetch(reinterpret_cast<const char*>(weights + 256), _MM_HINT_T0);
_mm_prefetch(reinterpret_cast<const char*>(input + 64), _MM_HINT_T0);
sum0 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights)))),
_mm512_loadu_ps(input), sum0);
sum1 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 16)))),
_mm512_loadu_ps(input + 16), sum1);
sum2 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 32)))),
_mm512_loadu_ps(input + 32), sum2);
sum3 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 48)))),
_mm512_loadu_ps(input + 48), sum3);
sum4 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 64)))),
_mm512_loadu_ps(input + 64), sum4);
sum5 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 80)))),
_mm512_loadu_ps(input + 80), sum5);
sum6 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 96)))),
_mm512_loadu_ps(input + 96), sum6);
sum7 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 112)))),
_mm512_loadu_ps(input + 112), sum7);
}
for (; i + 64 <= n; i += 64, weights += 64, input += 64) {
sum0 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights)))),
_mm512_loadu_ps(input), sum0);
sum1 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 16)))),
_mm512_loadu_ps(input + 16), sum1);
sum2 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 32)))),
_mm512_loadu_ps(input + 32), sum2);
sum3 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 48)))),
_mm512_loadu_ps(input + 48), sum3);
}
for (; i + 32 <= n; i += 32, weights += 32, input += 32) {
sum0 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights)))),
_mm512_loadu_ps(input), sum0);
sum1 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights + 16)))),
_mm512_loadu_ps(input + 16), sum1);
}
for (; i + 16 <= n; i += 16, weights += 16, input += 16)
sum0 = _mm512_fmadd_ps(_mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(weights)))),
_mm512_loadu_ps(input), sum0);
__m512 sum = _mm512_add_ps(_mm512_add_ps(_mm512_add_ps(sum0, sum1), _mm512_add_ps(sum2, sum3)),
_mm512_add_ps(_mm512_add_ps(sum4, sum5), _mm512_add_ps(sum6, sum7)));
float result = reduce_sum512(sum);
if (i < n) {
// Masked <16 tail: maskz loads cannot fault, same math as scalar.
// Heap rows may end at a page boundary, so no unmasked over-read.
const std::size_t k = n - i;
const __mmask16 m = static_cast<__mmask16>((1u << k) - 1u);
__m128i wb = _mm_maskz_loadu_epi8(m, weights);
__m512 wf = _mm512_cvtepi32_ps(_mm512_cvtepi8_epi32(wb));
__m512 a = _mm512_maskz_loadu_ps(m, input);
result += reduce_sum512(_mm512_mul_ps(wf, a));
}
return result;
}
float dot_int8_q8_vnni(const std::int8_t* weights, const std::int8_t* act,
const float* act_scales, std::size_t n) {
float sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0;
std::size_t i = 0;
const __m256i bias = _mm256_set1_epi8(static_cast<char>(0x80));
const __m256i ones = _mm256_set1_epi16(1);
for (; i + 128 <= n; i += 128, weights += 128, act += 128) {
_mm_prefetch(reinterpret_cast<const char*>(weights + 256), _MM_HINT_T0);
_mm_prefetch(reinterpret_cast<const char*>(act + 256), _MM_HINT_T0);
sum0 += static_cast<float>(block_dot32_vnni(weights, act, bias, ones)) * act_scales[i / 32];
sum1 += static_cast<float>(block_dot32_vnni(weights + 32, act + 32, bias, ones)) * act_scales[i / 32 + 1];
sum2 += static_cast<float>(block_dot32_vnni(weights + 64, act + 64, bias, ones)) * act_scales[i / 32 + 2];
sum3 += static_cast<float>(block_dot32_vnni(weights + 96, act + 96, bias, ones)) * act_scales[i / 32 + 3];
}
float result = (sum0 + sum1) + (sum2 + sum3);
for (; i + 64 <= n; i += 64, weights += 64, act += 64) {
// 64-wide fallback: two 32-blocks in order, sequential scalar adds.
// Bitwise-identical to two 32-wide iterations; halves tail-loop
// overhead (Q8 analogue of the fp32 kernels' 64-wide blocking). No
// prefetch: tail-resident, mirrors AVX2 Q8 (prefetch only in 128-loop).
result += static_cast<float>(block_dot32_vnni(weights, act, bias, ones)) * act_scales[i / 32];
result += static_cast<float>(block_dot32_vnni(weights + 32, act + 32, bias, ones)) * act_scales[i / 32 + 1];
}
for (; i + 32 <= n; i += 32, weights += 32, act += 32)
result += static_cast<float>(block_dot32_vnni(weights, act, bias, ones)) * act_scales[i / 32];
if (i < n) {
// Partial tail block, read only to the row's logical end. The scale
// is hoisted before the loop (start is 32-aligned with <32 left, so
// this is the same block index the post-loop form read).
const float scale = act_scales[i / 32];
int block_sum = 0;
for (std::size_t j = 0; i < n; ++i, ++j)
block_sum += static_cast<int>(weights[j]) * static_cast<int>(act[j]);
result += static_cast<float>(block_sum) * scale;
}
return result;
}
// int4-VNNI / fp4-VNNI: deliberately deferred (no kernels.hpp signature
// change, no dead code) — int4/fp4 stay on the AVX2 Q8 path. Both would fit
// this dpbusd ABI via pshufb LUT-decode to int8 then block_dot32_vnni above
// (int4 nibbles -> {-8..7}; fp4 E2M1 -> half-values 0,+-1,+-2,+-3,+-4,+-6,
// +-8,+-12 with the pre-halved E4M3 scale, exact like the AVX2 fp4 path),
// but the LUT cost dominates: the pshufb decode (mask+srli+shuffle+unpack)
// is identical on both paths and dpbusd only replaces the final pmaddwd,
// while a VNNI 4-bit kernel would additionally need an int16->int8 act
// down-convert (Int4Q8/Fp4Q8 ABI takes int16 acts; no int8-act 4-bit
// signature exists and quantize_row_i8/dispatch wiring lives in
// runtime.cpp, outside this TU's ownership) plus a two-scale (per-16 weight
// + per-32 act) epilogue per 32-block for fp4. Net: no win without a Matrix
// ABI change + new dispatch, and no VNNI silicon on this box to validate an
// otherwise unwired kernel. Revisit with Zen4/ADL silicon + PPL re-gate.
#else
// Compiler without VNNI support: scalar-correct implementation so the symbol
// always links. Dispatch (kernels.cpp) never selects it without VNNI macros,
// so this is dead code on such builds — correctness over speed here.
float dot_int8_q8_vnni(const std::int8_t* weights, const std::int8_t* act,
const float* act_scales, std::size_t n) {
float result = 0;
std::size_t i = 0;
for (; i + 32 <= n; i += 32, weights += 32, act += 32) {
int block_sum = 0;
for (std::size_t j = 0; j < 32; ++j)
block_sum += static_cast<int>(weights[j]) * static_cast<int>(act[j]);
result += static_cast<float>(block_sum) * act_scales[i / 32];
}
if (i < n) {
// Partial tail, read only to the row's logical end. Scale hoisted
// before the loop (start is 32-aligned with <32 left, so this equals
// the post-loop act_scales[i/32]); mirrors the VNNI fast path.
const float scale = act_scales[i / 32];
int block_sum = 0;
for (std::size_t j = 0; i < n; ++i, ++j)
block_sum += static_cast<int>(weights[j]) * static_cast<int>(act[j]);
result += static_cast<float>(block_sum) * scale;
}
return result;
}
#endif
} // namespace cism
|