test1111111 / native /kernels.hpp
spitfire4794's picture
Serve SurjoLabs/Surjo-50m-SFT-Only (int8, 2T) on 7860
92dcc4e verified
Raw History Blame Contribute Delete
11.4 kB
#pragma once
#include <cstddef>
#include <cstdint>
namespace cism {
using DotKernel = float (*)(const float*, const float*, std::size_t);
using Int8DotKernel = float (*)(const std::int8_t*, const float*, std::size_t);
// IEEE-754 binary16 conversion (round-to-nearest-even), portable software
// implementation for packing and scalar fallback (no F16C requirement).
std::uint16_t fp32_to_fp16(float value);
float fp16_to_fp32(std::uint16_t bits);
using Fp16DotKernel = float (*)(const std::uint16_t*, const float*, std::size_t);
// int4/fp4 dots consume a permuted activation stream (even/odd 32-blocks,
// built by the caller) for full blocks plus the original row for the scalar
// tail; the 5th/4th scale args are per-32 FP32 / per-16 E4M3 bytes.
using Int4DotKernel = float (*)(const std::uint8_t*, const float*, const float*, const float*, std::size_t);
// FP4 (E2M1 elements, E4M3 scale per 16 elements): nibbles, permuted + original
// activations, and scale bytes.
using Fp4DotKernel = float (*)(const std::uint8_t*, const float*, const float*, const std::uint8_t*, std::size_t);
// Quantized-activation kernels: the caller quantizes the shared activation
// to int16 per 32-element block (32767/absmax, block-dequant scales passed
// alongside); weights decode to int16 through pshufb LUTs and multiply
// accumulate in integer pmaddwd lanes with independent accumulators.
using Int8Q8DotKernel = float (*)(const std::int8_t*, const std::int16_t*, const float*, std::size_t);
using Int4Q8DotKernel = float (*)(const std::uint8_t*, const std::int16_t*, const float*, const float*, std::size_t);
using Fp4Q8DotKernel = float (*)(const std::uint8_t*, const std::int16_t*, const std::uint8_t*, const float*, std::size_t);
DotKernel fp32_kernel();
Int8DotKernel int8_kernel();
Fp16DotKernel fp16_kernel();
Int4DotKernel int4_kernel();
Fp4DotKernel fp4_kernel();
// Quantized-activation selectors: null when AVX2 is unavailable (callers
// fall back to the fp32-activation kernels above).
Int8Q8DotKernel int8_q8_kernel();
Int4Q8DotKernel int4_q8_kernel();
Fp4Q8DotKernel fp4_q8_kernel();
const char* kernel_name();
// Portable-ISA dispatch (Intel/AMD/x86 + ARM phones).
// Highest SIMD tier this binary can actually execute on this CPU:
// "scalar" (any CPU) < "avx2" (x86-64, FMA) < "avx-vnni" (Zen4+/ADL+,
// integer MAC fast path) < "avx512-vnni" (server AVX512) / "neon" (ARM64
// phones, Apple Silicon, Raspberry Pi). kernel_name() above keeps reporting
// the fp32-kernel family ("scalar"/"avx2"/"neon"); kernel_variant() reports
// this overall tier for telemetry and tests.
const char* kernel_variant();
bool has_avx2_cpu();
bool has_f16c_cpu();
bool has_sse41_cpu();
bool has_avx_cpu();
bool has_avx_vnni_cpu();
bool has_avx512_vnni_cpu();
bool has_neon_cpu();
// int8-activation VNNI path: quantize_row_i8 shares quantize_row's blocking
// (int8 127/absmax per 32, one fp32 dequant scale per block) so the VNNI
// kernel sees the same blocking the int16 path uses. Selector is null unless
// the binary was built with VNNI support AND the CPU has it (otherwise the
// int16 Q8 path above is used, so pre-VNNI machines are untouched).
void quantize_row_i8(const float* input, std::size_t n, std::int8_t* values, float* scales);
void quantize_row_i8_avx2(const float* input, std::size_t n, std::int8_t* values, float* scales);
using Int8Q8VnniKernel = float (*)(const std::int8_t*, const std::int8_t*, const float*, std::size_t);
Int8Q8VnniKernel int8_q8_vnni_kernel();
float dot_scalar(const float* a, const float* b, std::size_t n);
float dot_fp16_scalar(const std::uint16_t* weights, const float* input, std::size_t n);
float dot_int8_scalar(const std::int8_t* weights, const float* input, std::size_t n);
float dot_int4_scalar(const std::uint8_t* weights, const float* input, const float* scales, std::size_t n);
float dot_fp4_scalar(const std::uint8_t* weights, const float* input, const std::uint8_t* scales, std::size_t n);
// 5-arg adapters over the 4-arg scalars (ignore the permuted buffer) so the
// permuted kernel type keeps a working non-AVX2 fallback.
float dot_int4_scalar5(const std::uint8_t* weights, const float* act_perm,
const float* input, const float* scales, std::size_t n);
float dot_fp4_scalar5(const std::uint8_t* weights, const float* act_perm,
const float* input, const std::uint8_t* scales, std::size_t n);
// E2M1 quantization support shared by the packer and the kernels.
// element_lut: raw nibble -> E2M1 value times two as int8 (exact small ints).
// scale_lut: E4M3 bit pattern -> float scale, already halved so a decoded
// element (half value) times the table entry equals E2M1 * E4M3 exactly.
// encode_scale: positive float -> E4M3 bit pattern, round-half-even like
// torch.float8_e4m3fn, saturating at 448.
const std::int8_t* fp4_element_lut();
// Kernel tuning notes (Zen3 AVX2+FMA, generic /arch:AVX2, no VNNI):
// - int8 is bandwidth-bound; 4-bit is kernel-bound (~26 uops/32 structural).
// - Inner blocking below (64-wide unroll) and T0 prefetches are the only
// kernel-internal levers that keep scalar-fallback bitwise semantics.
// - Row-slice / thread-threshold / GEMM-blocking / SiLU-fusion live in
// runtime.cpp (not owned here); this header documents the shared constants
// so a future runtime wiring stays consistent. SIMD order may differ from
// scalar; numeric changes stay PPL-gated (see VALIDATION.md noise note).
constexpr std::size_t kParallelThreshold = 1u << 16; // rows*cols, runtime-owned
constexpr std::size_t kDotUnroll = 64; // kernel inner blocking
constexpr std::size_t kPrefetchDistance = 512; // bytes, T0, never faults
// Permuted-activation helper for int4/fp4 (even/odd 32-blocks). Scalar
// semantics; the AVX2 build offers an unrolled variant. Not wired to the
// runtime path here (session-persistent scratch already reuses the buffer);
// provided so future runtime wiring has one canonical implementation.
void permute_act32_blocked(const float* input, std::size_t n, float* out);
// Gated SiLU*up epilogue (stable sigmoid, matches Session::forward_tokens).
// Test-gated only: silu_fusion_enabled() is false unless CISM_FUSE_SILU=1,
// and the runtime path does not call these (prior A/B showed fusion ~7%
// slower on Zen3 streaming hardware). Scalar is the bitwise reference.
void silu_mul_scalar(const float* gate, const float* up, float* out, std::size_t n);
bool silu_fusion_enabled();
// Vector-exp activation blocks for the decode hot path (Surjo GDN/MLP/XSA).
// AVX2 TU evaluates exp with a ≤1-ULP polynomial (deterministic: same input
// bits always give same output bits); the scalar fallback uses libm and is
// the bitwise reference on non-AVX2 machines. In-place forms allow x==out.
// Unified stable sigmoid 1/(1+exp(-x)) (exact for x>=0, ≤1 ULP elsewhere).
// Numeric changes vs libm stay PPL-gated (VALIDATION.md noise note).
void act_exp(float* x, std::size_t n);
void act_silu(float* x, std::size_t n);
void act_silu_mul(const float* gate, const float* up, float* out, std::size_t n);
void act_sigmoid(float* x, std::size_t n);
void act_sigmoid_mul(float* o, const float* g, std::size_t n);
// Clamp-free SiLU*up for the dense path (no +-15 clamp there).
void act_silu_mul_plain(const float* gate, const float* up, float* out, std::size_t n);
// Exact GELU for the FWKV path (0.5*x*(1+erf(x/sqrt(2))), PyTorch exact).
void act_gelu(float* x, std::size_t n);
#ifdef CISM_HAVE_AVX2
void permute_act32_avx2(const float* input, std::size_t n, float* out);
void silu_mul_avx2(const float* gate, const float* up, float* out, std::size_t n);
#endif
const float* fp4_scale_lut();
float fp4_decode_scale(std::uint8_t bits);
std::uint8_t fp4_encode_scale(float value);
#ifdef CISM_HAVE_AVX2
float dot_avx2(const float* a, const float* b, std::size_t n);
float dot_fp16_avx2(const std::uint16_t* weights, const float* input, std::size_t n);
// Vector-exp act blocks (defined in kernels_avx2.cpp; scalar twins below).
void act_exp_avx2(float* x, std::size_t n);
void act_silu_avx2(float* x, std::size_t n);
void act_silu_mul_avx2(const float* gate, const float* up, float* out, std::size_t n);
void act_silu_mul_plain_avx2(const float* gate, const float* up, float* out, std::size_t n);
void act_gelu_avx2(float* x, std::size_t n);
void act_sigmoid_avx2(float* x, std::size_t n);
void act_sigmoid_mul_avx2(float* o, const float* g, std::size_t n);
float dot_int8_avx2(const std::int8_t* weights, const float* input, std::size_t n);
float dot_int4_avx2(const std::uint8_t* weights, const float* act_perm, const float* act_orig, const float* scales, std::size_t n);
float dot_fp4_avx2(const std::uint8_t* weights, const float* act_perm, const float* act_orig, const std::uint8_t* scales, std::size_t n);
float dot_int8_q8_avx2(const std::int8_t* weights, const std::int16_t* act, const float* act_scales, std::size_t n);
float dot_int4_q8_avx2(const std::uint8_t* weights, const std::int16_t* act, const float* scales,
const float* act_scales, std::size_t n);
float dot_fp4_q8_avx2(const std::uint8_t* weights, const std::int16_t* act,
const std::uint8_t* scales, const float* act_scales, std::size_t n);
#endif
// AVX-VNNI / AVX512-VNNI integer-MAC path (kernels_vnni.cpp; scalar-correct
// fallback when the compiler lacks VNNI — dispatch never selects it then).
float dot_int8_q8_vnni(const std::int8_t* weights, const std::int8_t* act,
const float* act_scales, std::size_t n);
// AVX512 zmm fp32 dot (kernels_vnni.cpp; needs AVX512F, gated on VNNI
// presence like the rest of that TU). Different chunking/reduce from AVX2:
// matches within fp32 rounding (PPL-gated), not bitwise-identical.
#ifdef CISM_HAVE_AVX512_VNNI
float dot_fp32_avx512(const float* a, const float* b, std::size_t n);
float dot_int8_avx512(const std::int8_t* weights, const float* input, std::size_t n);
#endif
#if defined(CISM_HAVE_NEON) && defined(__aarch64__)
float dot_neon(const float* a, const float* b, std::size_t n);
float dot_int8_neon(const std::int8_t* weights, const float* input, std::size_t n);
float dot_int4_neon(const std::uint8_t* weights, const float* act_perm, const float* act_orig, const float* scales, std::size_t n);
float dot_fp4_neon(const std::uint8_t* weights, const float* act_perm, const float* act_orig, const std::uint8_t* scales, std::size_t n);
void permute_act32_neon(const float* input, std::size_t n, float* out);
#endif
// SSE4.1 + AVX fallback TU (kernels_sse.cpp) for pre-AVX2 x86
// (Nehalem/Sandy/Bulldozer era). Same signatures as the AVX2 kernels;
// dispatch prefers AVX2 whenever has_avx2_cpu() is true.
#ifdef CISM_HAVE_SSE41
float dot_sse41(const float* a, const float* b, std::size_t n);
float dot_int8_sse41(const std::int8_t* weights, const float* input, std::size_t n);
float dot_int4_sse41(const std::uint8_t* weights, const float* act_perm, const float* act_orig, const float* scales, std::size_t n);
float dot_fp4_sse41(const std::uint8_t* weights, const float* act_perm, const float* act_orig, const std::uint8_t* scales, std::size_t n);
#endif
#ifdef CISM_HAVE_AVX
float dot_avx_fp32(const float* a, const float* b, std::size_t n);
#endif
} // namespace cism