repoverse / unify /tests /test_compute.cpp
SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
3ce1a40 verified
Raw History Blame Contribute Delete
7.21 kB
#include "test_main.h"
#include "../engine/wasm/compute.h"
#include "../engine/serialization/stream.h"
using namespace unify;
static const char* kVadd = R"(
__global__ void vadd(const float* A, const float* B, float* C, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
C[i] = A[i] + B[i];
}
}
)";
TEST(kernel_pipeline_produces_ast_ir_and_wasm) {
kernel::CompiledKernel k = kernel::compile(kVadd);
CHECK(k.ok);
CHECK(k.name == "vadd");
CHECK(k.params.size() == 4);
CHECK(k.ir_text.find("store.f32 %p2[%i] = (load.f32 %p0[%i] +.f32 load.f32 %p1[%i])") != std::string::npos);
wasm::Module m;
std::string err;
CHECK(wasm::decode(k.wasm, m, err));
CHECK(m.memory_imported);
CHECK(m.functions.size() == 2);
}
TEST(kernel_vadd_executes_in_wasm_runtime) {
ComputeRuntime rt;
std::string err;
KernelHandle k = rt.compile_and_load(kVadd, err);
CHECK(k);
const int n = 1000;
std::vector<float> a(n), b(n), c(n, -1);
for (int i = 0; i < n; i++) { a[size_t(i)] = float(i) * 0.5f; b[size_t(i)] = 100.0f - float(i); }
BufferHandle A = rt.buffer_create(n * 4), B = rt.buffer_create(n * 4), C = rt.buffer_create(n * 4);
CHECK(rt.buffer_upload(A, a.data(), n * 4));
CHECK(rt.buffer_upload(B, b.data(), n * 4));
// 4 blocks of 256 threads = 1024 threads; the bounds check in the kernel skips the extra 24.
DispatchResult r = rt.dispatch(k, 4, 256, {KernelArg::buf(A), KernelArg::buf(B), KernelArg::buf(C), KernelArg::integer(n)});
CHECK(r.ok);
CHECK(rt.buffer_download(C, c.data(), n * 4));
bool all = true;
for (int i = 0; i < n; i++) all &= c[size_t(i)] == a[size_t(i)] + b[size_t(i)];
CHECK(all);
}
TEST(kernel_loops_locals_intrinsics_and_casts) {
const char* src = R"(
__global__ void stats(const float* x, float* out, int n) {
if (threadIdx.x != 0) return;
float sum = 0.0f;
float hi = -1000000.0f;
int count = 0;
for (int i = 0; i < n; i++) {
if (x[i] < 0.0f) continue;
sum += x[i];
hi = fmaxf(hi, x[i]);
count = count + 1;
if (count == 4) break;
}
out[0] = sum;
out[1] = hi;
out[2] = (float)count;
out[3] = sqrtf(16.0f) + 2 * 3;
})";
ComputeRuntime rt;
std::string err;
KernelHandle k = rt.compile_and_load(src, err);
CHECK(k);
if (!k) std::printf(" compile error: %s\n", err.c_str());
float in[] = {1, -5, 2, 9, -1, 3, 100};
BufferHandle X = rt.buffer_create(sizeof in), O = rt.buffer_create(16);
rt.buffer_upload(X, in, sizeof in);
CHECK(rt.dispatch(k, 1, 1, {KernelArg::buf(X), KernelArg::buf(O), KernelArg::integer(7)}).ok);
float out[4];
rt.buffer_download(O, out, 16);
CHECK_NEAR(out[0], 1 + 2 + 9 + 3, 0); // negative skipped by continue; stops at 4 values
CHECK_NEAR(out[1], 9, 0);
CHECK_NEAR(out[2], 4, 0);
CHECK_NEAR(out[3], 10, 0); // constant-folded at compile time
CHECK(rt.kernel_info(k)->folded_constants > 0);
}
TEST(kernel_compile_errors_have_line_numbers) {
auto k = kernel::compile("__global__ void f(float* a) {\n a[0] = b;\n}");
CHECK(!k.ok);
CHECK(k.error.find("line 2") != std::string::npos);
CHECK(k.error.find("'b'") != std::string::npos);
auto k2 = kernel::compile("__global__ void f(float* a) { a = 1; }");
CHECK(!k2.ok);
}
TEST(kernel_out_of_bounds_traps_instead_of_crashing) {
ComputeRuntime rt(1, 1);
std::string err;
KernelHandle k = rt.compile_and_load("__global__ void w(float* a, int i) { a[i] = 1.0f; }", err);
BufferHandle A = rt.buffer_create(16);
DispatchResult r = rt.dispatch(k, 1, 1, {KernelArg::buf(A), KernelArg::integer(1 << 24)});
CHECK(!r.ok);
CHECK(r.error.find("out of bounds") != std::string::npos);
DispatchResult r2 = rt.dispatch(k, 1, 1, {KernelArg::integer(3), KernelArg::integer(0)});
CHECK(!r2.ok); // scalar passed where a buffer is required
}
TEST(kernel_handles_are_validated) {
ComputeRuntime rt;
BufferHandle b = rt.buffer_create(64);
CHECK(rt.buffer_destroy(b));
float x = 1;
CHECK(!rt.buffer_upload(b, &x, 4)); // stale handle
CHECK(!rt.dispatch(KernelHandle{}, 1, 1, {}).ok);
BufferHandle b2 = rt.buffer_create(64);
CHECK(b2.index() == b.index() && b2 != b); // slot reused, generation bumped
}
TEST(wasm_interpreter_handles_loops_calls_and_traps) {
// Hand-assembled module: (func $fact (param i32) (result i32) recursive) and (func $div (param i32 i32) (result i32)).
std::vector<uint8_t> m = {0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00,
0x01, 0x0C, 0x02, 0x60, 0x01, 0x7F, 0x01, 0x7F, 0x60, 0x02, 0x7F, 0x7F, 0x01, 0x7F, // types
0x03, 0x03, 0x02, 0x00, 0x01, // funcs
0x07, 0x0E, 0x02, 0x04, 'f', 'a', 'c', 't', 0x00, 0x00, 0x03, 'd', 'i', 'v', 0x00, 0x01,
0x0A, 0x21, 0x02,
// fact: if (n <= 1) 1 else n * fact(n-1)
0x17, 0x00, 0x20, 0x00, 0x41, 0x01, 0x4C, 0x04, 0x7F, 0x41, 0x01, 0x05, 0x20, 0x00, 0x20, 0x00, 0x41, 0x01, 0x6B, 0x10, 0x00, 0x6C, 0x0B, 0x0B,
// div: a / b (signed)
0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6D, 0x0B};
auto mod = std::make_shared<wasm::Module>();
std::string err;
CHECK(wasm::decode(m, *mod, err));
if (!err.empty()) std::printf(" decode error: %s\n", err.c_str());
wasm::Instance inst;
CHECK(inst.instantiate(mod, nullptr, {}, err));
auto r = inst.call("fact", {wasm::Value::i32(10)});
CHECK(r.ok && r.results.size() == 1 && r.results[0].as_i32() == 3628800);
auto d = inst.call("div", {wasm::Value::i32(7), wasm::Value::i32(0)});
CHECK(!d.ok && d.trap == "integer divide by zero");
inst.max_call_depth = 5;
CHECK(!inst.call("fact", {wasm::Value::i32(50)}).ok);
inst.max_call_depth = 512;
inst.fuel = 20;
CHECK(inst.call("fact", {wasm::Value::i32(10)}).trap == "fuel exhausted");
}
TEST(wasm_rejects_malformed_modules) {
wasm::Module m;
std::string err;
CHECK(!wasm::decode({0x00, 'a', 's', 'm'}, m, err));
CHECK(!wasm::decode({0x00, 'a', 's', 'm', 1, 0, 0, 0, 0x0A, 0x05, 0x01, 0x03, 0x00, 0xFF, 0x0B}, m, err));
}
TEST(compute_state_round_trips) {
ComputeRuntime rt;
std::string err;
KernelHandle k = rt.compile_and_load(kVadd, err);
BufferHandle A = rt.buffer_create(16), B = rt.buffer_create(16), C = rt.buffer_create(16);
float a[4] = {1, 2, 3, 4}, b[4] = {10, 20, 30, 40};
rt.buffer_upload(A, a, 16); rt.buffer_upload(B, b, 16);
BinaryWriter w;
rt.serialize(w);
ComputeRuntime restored;
BinaryReader r(w.data());
CHECK(restored.deserialize(r, err));
CHECK(restored.buffer_valid(A) && restored.kernel_valid(k));
CHECK(restored.dispatch(k, 1, 4, {KernelArg::buf(A), KernelArg::buf(B), KernelArg::buf(C), KernelArg::integer(4)}).ok);
float c[4];
restored.buffer_download(C, c, 16);
CHECK(c[0] == 11 && c[3] == 44);
BufferHandle D = restored.buffer_create(16); // allocator state restored: no overlap with live buffers
CHECK(D.index() == 3);
}