Files
EVEngine/test/tensor_quant.cpp
T
西风逍遥游 cff9bb4d0c tensor: AITemplate-style compile pipeline, gpgpu Sequence, mini-LLM demo and weight-only quantization
Refactor the tensor module around AITemplate-style static compilation:

- Optimizer: DCE, constant folding, elementwise chain fusion, matmul/conv
  bias+activation epilogue fusion, liveness-based static memory planning,
  topological group execution order.
- KernelGen: shape-baked GLSL per fused group; two-pass softmax/layernorm;
  shared-memory SDPA; naive + 16x16 tiled matmul templates.
- GpuBackend: arena buffer allocation per memory plan, matmul autotuning,
  single command-buffer submission via the new gpgpu::Sequence (uploads,
  dispatches and downloads recorded once per run, Kompute-style).
- shaderc in-process GLSL->SPIR-V on Windows (no glslc.exe dependency),
  with glslc spawn fallback.
- CPU reference kernels shared by eager and the compiled interpreter;
  no-Vulkan fallback keeps compiled graphs runnable headless.

Neural-network coverage: softmax/logsoftmax, layernorm/rmsnorm (with
weight/bias), gelu/silu, conv1d/conv2d, pool, embedding, concat/slice,
axis reduces, argmax, cast, fused SDPA (+masked), resize2d, batched
matmul, seeded random.

Weight-only quantization (activations stay fp32):
- fp16 (IEEE half), fp8 (e4m3), fp4 (e2m1), int8, int4 with per-group
  block scales (MXFP-style: scale = maxAbs / format max magnitude).
- Quantized weights capture into graph Const nodes; CPU interpreter
  dequantizes at materialization; GPU matmul/embedding kernels dequantize
  inline from packed uint buffers (naive matmul variant).
- TF::quantizeWeight API + script binding.

Mini-LLM validation (TinyStories-gpt2-3M, 8 layers d=64):
- test/tensor_llm.cpp builds the full GPT-2 graph from engine ops and
  matches numpy reference: probs within 3e-7, greedy generation 24/24
  identical (~100 ms/token CPU), compiled path exact.
- tensor.llm.quantizedDtypes: fp16/int8/fp8/int4 all keep 16/16 greedy
  top-1 (fp4 16/16 with proper block scaling), weights 27.6 MB -> 3.3-13 MB.
- scripts/export_tiny_gpt2.py converts a PyTorch .bin checkpoint (no torch
  dependency) plus GPT-2 tokenizer into engine assets + numpy reference.

Tests: gpgpu 8/8, tensor 31/31 (original suite) + tensor.llm.* + tensor.quant.*
Assets in test/assets/mini_llm are gitignored (regenerate via the script).
2026-08-18 17:42:38 +08:00

199 lines
5.8 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "tensor/Graph.h"
#include "tensor/Quant.h"
#include "tensor/TF.h"
#include "tensor/Tensor.h"
#include <cmath>
#include <cstdio>
#include <memory>
#include <vector>
using namespace eve::tensor;
namespace {
std::vector<float> randData(int n, unsigned seed) {
std::vector<float> v(static_cast<size_t>(n));
unsigned s = seed;
for (int i = 0; i < n; ++i) {
s = s * 1664525u + 1013904223u;
v[static_cast<size_t>(i)] = float(int(s % 2001) - 1000) / 1000.f; // [-1, 1]
}
return v;
}
Tensor *fromData(const std::vector<float> &d, int d0, int d1) {
auto *t = new Tensor(d0, d1);
for (size_t i = 0; i < d.size(); ++i) t->set(int(i), d[static_cast<size_t>(i)]);
return t;
}
float maxAbsDiff(const Tensor *a, const Tensor *b) {
float m = 0.f;
for (int i = 0; i < a->getSize(); ++i) m = std::max(m, std::fabs(a->get(i) - b->get(i)));
return m;
}
const char *dtypeTag(DType dt) {
switch (dt) {
case DType::Fp16: return "fp16";
case DType::Fp8E4M3: return "fp8";
case DType::Fp4E2M1: return "fp4";
case DType::Int8: return "int8";
case DType::Int4: return "int4";
default: return "?";
}
}
} // namespace
TEST_CASE("tensor.quant.roundtrip") {
auto *tf = TF::create();
const std::vector<float> src = randData(32 * 48, 7);
Tensor *ref = fromData(src, 32, 48);
const struct {
DType dt;
float bound;
} cases[] = {
{DType::Fp16, 1e-3f},
{DType::Int8, 0.03f},
{DType::Fp8E4M3, 0.08f},
{DType::Int4, 0.3f},
{DType::Fp4E2M1, 0.35f},
};
for (const auto &c : cases) {
Tensor *q = tf->quantizeWeight(ref, dtypeTag(c.dt), 16);
REQUIRE(q != nullptr);
CHECK(q->isQuantized());
CHECK_EQ(std::string(q->getDtype()), std::string(dtypeTag(c.dt)));
float maxErr = 0.f;
for (int i = 0; i < q->getSize(); ++i)
maxErr = std::max(maxErr, std::fabs(q->get(i) - src[static_cast<size_t>(i)]));
std::fprintf(stderr, "[quant] %s round-trip max err = %.4f (bound %.3f)\n",
dtypeTag(c.dt), maxErr, c.bound);
CHECK(maxErr < c.bound);
delete q;
}
delete ref;
}
TEST_CASE("tensor.quant.eagerMatmul") {
auto *tf = TF::create();
const std::vector<float> ad = randData(4 * 8, 11);
const std::vector<float> bd = randData(8 * 16, 23);
Tensor *a = fromData(ad, 4, 8);
Tensor *b = fromData(bd, 8, 16);
Tensor *baseline = a->matmul(b);
const struct {
DType dt;
float bound;
} cases[] = {
{DType::Fp16, 0.01f},
{DType::Int8, 0.15f},
{DType::Fp8E4M3, 0.35f},
{DType::Int4, 1.2f},
{DType::Fp4E2M1, 1.4f},
};
for (const auto &c : cases) {
Tensor *qb = tf->quantizeWeight(b, dtypeTag(c.dt), 8);
Tensor *out = a->matmul(qb);
const float diff = maxAbsDiff(out, baseline);
std::fprintf(stderr, "[quant] eager %s matmul max diff = %.4f (bound %.2f)\n",
dtypeTag(c.dt), diff, c.bound);
CHECK(diff < c.bound);
delete qb;
delete out;
}
delete a;
delete b;
delete baseline;
}
TEST_CASE("tensor.quant.compiledMatmul") {
auto *tf = TF::create();
const std::vector<float> ad = randData(4 * 8, 31);
const std::vector<float> bd = randData(8 * 16, 43);
Tensor *a = fromData(ad, 4, 8);
Tensor *b = fromData(bd, 8, 16);
for (const char *tag : {"fp16", "int8", "fp8", "int4", "fp4"}) {
Tensor *qb = tf->quantizeWeight(b, tag, 8);
Tensor *eager = a->matmul(qb);
auto *fn = tf->func();
Tensor *ph = fn->input2(4, 8);
fn->setOutput(tf->matmul(ph, qb)); // quantized const captured as a graph const
std::unique_ptr<CompiledFunction> cf(fn->compile());
Tensor *out = cf->run1(a);
const float diff = maxAbsDiff(out, eager);
std::fprintf(stderr, "[quant] compiled %s matmul vs eager diff = %.6f\n", tag, diff);
// CPU: identical dequant+double-accumulate loops -> ~0. GPU: fp32
// accumulation differs from eager's double accumulation by ~1e-3.
CHECK(diff < 0.01f);
delete qb;
delete eager;
delete out;
}
delete a;
delete b;
}
TEST_CASE("tensor.quant.embedding") {
auto *tf = TF::create();
const std::vector<float> td = randData(10 * 6, 51);
Tensor *table = fromData(td, 10, 6);
const float idxv[4] = {0.f, 3.f, 9.f, 5.f};
Tensor *idx = new Tensor(4);
for (int i = 0; i < 4; ++i) idx->set1(i, idxv[i]);
Tensor *baseline = tf->embedding(table, idx);
for (const char *tag : {"fp16", "int8", "fp8", "int4", "fp4"}) {
Tensor *qt = tf->quantizeWeight(table, tag, 6);
Tensor *out = tf->embedding(qt, idx);
const float diff = maxAbsDiff(out, baseline);
std::fprintf(stderr, "[quant] eager %s embedding max diff = %.4f\n", tag, diff);
CHECK(diff < 0.4f);
auto *fn = tf->func();
Tensor *ph = fn->input1(4);
fn->setOutput(tf->embedding(qt, ph));
std::unique_ptr<CompiledFunction> cf(fn->compile());
Tensor *cout = cf->run1(idx);
const float cdiff = maxAbsDiff(cout, out);
CHECK(cdiff < 0.01f);
delete qt;
delete out;
delete cout;
}
delete table;
delete idx;
delete baseline;
}
TEST_CASE("tensor.quant.guards") {
auto *tf = TF::create();
Tensor *t = tf->fill2(2, 2, 1.f);
Tensor *q = tf->quantizeWeight(t, "int8", 2);
bool threw = false;
try {
(void)q->data();
} catch (...) {
threw = true;
}
CHECK(threw);
threw = false;
try {
q->set(0, 1.f);
} catch (...) {
threw = true;
}
CHECK(threw);
delete q;
delete t;
}