Files
EVEngine/test/tensor_onnx_gpu.cpp
T

298 lines
12 KiB
C++

#include "common/config.h"
#if !defined(EVENGINE_WEBGPU)
#include <cstring>
#include <memory>
#include "gpgpu/ComputeProgram.h"
#include "gpgpu/ComputeShader.h"
#include "gpgpu/Gpgpu.h"
#include "gpgpu/GpuBuffer.h"
#include "gpgpu/Sequence.h"
#include "graphics/Graphics.h"
#include "graphics/vulkan/Graphics.h"
#include "tensor/OnnxCompute.h"
#include "tensor/OnnxModel.h"
#include "zeroerr/unittest.h"
namespace {
void initialize() {
static bool initialized = false;
if (!initialized) {
eve::graphics::Graphics::create()->initHeadless(64, 64);
initialized = true;
}
}
using Bytes = std::vector<uint8_t>;
void number(Bytes& b, uint64_t n) {
while (n >= 128) {
b.push_back(uint8_t(n) | 128);
n >>= 7;
}
b.push_back(uint8_t(n));
}
void integer(Bytes& b, int tag, uint64_t n) {
number(b, tag * 8);
number(b, n);
}
void field(Bytes& b, int tag, const Bytes& value) {
number(b, tag * 8 + 2);
number(b, value.size());
b.insert(b.end(), value.begin(), value.end());
}
void text(Bytes& b, int tag, const std::string& s) { field(b, tag, Bytes(s.begin(), s.end())); }
Bytes chain(size_t count = 4096, float increment = 1, bool invalidReshape = false, bool mixed = false,
int stages = 16) {
Bytes graph, tensor, raw(count * sizeof(float));
for (size_t i = 0; i < count; ++i) {
float f = float(i);
std::memcpy(raw.data() + i * 4, &f, 4);
}
integer(tensor, 1, count);
integer(tensor, 2, 1);
text(tensor, 8, "x");
field(tensor, 9, raw);
field(graph, 5, tensor);
Bytes bias, one(4);
float f = increment;
std::memcpy(one.data(), &f, 4);
integer(bias, 2, 1);
text(bias, 8, "one");
field(bias, 9, one);
field(graph, 5, bias);
std::string previous = "x";
for (int i = 0; i < stages; ++i) {
Bytes n;
text(n, 1, previous);
text(n, 1, "one");
auto next = "v" + std::to_string(i);
text(n, 2, next);
text(n, 4, mixed ? std::vector<std::string>{"Add", "Mul", "Sub", "Div"}[i % 4] : "Add");
field(graph, 1, n);
previous = next;
Bytes alias;
text(alias, 1, previous);
previous += "alias";
text(alias, 2, previous);
text(alias, 4, "Identity");
field(graph, 1, alias);
}
if (invalidReshape) {
Bytes t, raw(8), node;
int64_t dimension = int64_t(count) + 1;
std::memcpy(raw.data(), &dimension, 8);
integer(t, 1, 1);
integer(t, 2, 7);
text(t, 8, "badShape");
field(t, 9, raw);
field(graph, 5, t);
text(node, 1, previous);
text(node, 1, "badShape");
text(node, 2, "invalid");
text(node, 4, "Reshape");
field(graph, 1, node);
previous = "invalid";
}
Bytes out, opset, model;
text(out, 1, previous);
field(graph, 12, out);
integer(opset, 2, 14);
integer(model, 1, 7);
field(model, 8, opset);
field(model, 7, graph);
return model;
}
} // namespace
TEST_CASE("tensor.onnx.gpu.residentChain") {
initialize();
auto provider = eve::tensor::createOnnxGpuCompute();
REQUIRE(provider.ok());
auto model = eve::tensor::OnnxModel::load(chain());
REQUIRE(model.ok());
for (int repeat = 0; repeat < 2; ++repeat) {
auto r = model.value()->runGpu({}, *provider.value());
REQUIRE(r.ok());
CHECK(r.value().dispatches == 16);
CHECK(r.value().transfers.uploads == (repeat == 0 ? 2 : 0));
CHECK(r.value().transfers.shaderCompilations == (repeat == 0 ? 1 : 0));
CHECK(r.value().transfers.downloads == 1);
CHECK(r.value().transfers.submissions == 1);
const auto& raw = r.value().outputs[0].tensor.bytes;
for (size_t i = 0; i < 4096; ++i) {
float f;
std::memcpy(&f, raw.data() + i * 4, 4);
REQUIRE(f == float(i + 16));
}
}
}
TEST_CASE("gpgpu.sequence.stagingSizeReuse") {
initialize();
auto* gp = eve::gpgpu::Gpgpu::create();
REQUIRE(gp->isAvailable());
std::unique_ptr<eve::gpgpu::Sequence> seq(gp->newSequence());
for (const auto& sizes :
std::vector<std::vector<size_t>>{{4, 4096, 64}, {4096, 4, 64}, {8192, 8, 16384, 4}, {4, 16384, 8, 8192}}) {
std::vector<std::unique_ptr<eve::gpgpu::GpuBuffer>> device, staging;
std::vector<Bytes> expected;
seq->begin();
for (size_t i = 0; i < sizes.size(); ++i) {
Bytes src(sizes[i], uint8_t(i + 31));
expected.push_back(src);
device.emplace_back(gp->newBuffer(int(sizes[i])));
staging.emplace_back(gp->newBuffer(int(sizes[i]), "staging"));
seq->recordUpload(device.back().get(), src.data(), src.size());
// recordUpload promises an immediate copy, including in a multi-upload batch.
std::fill(src.begin(), src.end(), 0);
seq->recordDownload(device.back().get(), staging.back().get(), sizes[i]);
}
REQUIRE(seq->submitAsync() == eve::gpgpu::SequenceStatus::Submitted);
REQUIRE(seq->wait() == eve::gpgpu::SequenceStatus::Complete);
for (size_t i = 0; i < sizes.size(); ++i) {
Bytes actual(sizes[i]);
staging[i]->downloadBytes(actual.data(), actual.size());
REQUIRE(actual == expected[i]);
}
}
}
TEST_CASE("gpgpu.sequence.cancelRecordingRetiresDescriptors") {
initialize();
auto* gp = eve::gpgpu::Gpgpu::create();
std::unique_ptr<eve::gpgpu::GpuBuffer> buffer(gp->newBuffer(4));
std::unique_ptr<eve::gpgpu::ComputeShader> shader(
gp->newShader("#version 450\nlayout(local_size_x=64)in;layout(std430,binding=0)buffer B{float x[];};void "
"main(){if(gl_GlobalInvocationID.x==0)x[0]*=2.0;}"));
float value = 1;
buffer->uploadBytes(&value, 4);
for (int i = 0; i < 80; ++i) {
std::unique_ptr<eve::gpgpu::Sequence> canceled(gp->newSequence());
canceled->begin();
shader->bindBuffer(0, buffer.get());
canceled->recordDispatch(shader.get(), 1);
}
std::unique_ptr<eve::gpgpu::Sequence> seq(gp->newSequence());
std::unique_ptr<eve::gpgpu::GpuBuffer> staging(gp->newBuffer(4, "staging"));
seq->begin();
shader->bindBuffer(0, buffer.get());
seq->recordDispatch(shader.get(), 1);
seq->recordDownload(buffer.get(), staging.get(), 4);
seq->submit();
staging->downloadBytes(&value, 4);
CHECK(value == 2);
}
TEST_CASE("tensor.onnx.gpu.sessionShapesAndLifetime") {
auto* previous = eve::graphics::Graphics::create();
auto device = std::make_unique<eve::graphics::vulkan::Graphics>();
eve::ModuleManager::insert("Graphics", device.get());
struct Restore {
eve::graphics::Graphics* previous;
~Restore() { eve::ModuleManager::insert("Graphics", previous); }
} restore{previous};
device->initHeadless(64, 64);
auto gpu = eve::tensor::createOnnxGpuCompute();
REQUIRE(gpu.ok());
auto original = eve::tensor::OnnxModel::load(chain());
REQUIRE(original.ok());
auto alternate = eve::tensor::OnnxModel::load(chain(2048, 2));
REQUIRE(alternate.ok());
for (int pass = 0; pass < 3; ++pass) {
auto& model = pass == 1 ? alternate : original;
auto r = model.value()->runGpu({}, *gpu.value());
REQUIRE(r.ok());
CHECK(r.value().transfers.shaderCompilations == (pass < 2 ? 1 : 0));
const size_t count = pass == 1 ? 2048 : 4096;
const float added = pass == 1 ? 32.f : 16.f;
REQUIRE(r.value().outputs[0].tensor.bytes.size() == count * 4);
for (size_t i = 0; i < count; ++i) {
float actual;
std::memcpy(&actual, r.value().outputs[0].tensor.bytes.data() + i * 4, 4);
REQUIRE(actual == float(i) + added);
}
}
auto failing = eve::tensor::OnnxModel::load(chain(4096, 1, true));
REQUIRE(failing.ok());
auto failure = failing.value()->runGpu({}, *gpu.value());
REQUIRE(!failure.ok());
const std::vector<std::string> intermediate{"v15alias"};
auto recovered = failing.value()->runGpu({}, *gpu.value(), intermediate);
REQUIRE(recovered.ok());
CHECK(recovered.value().transfers.shaderCompilations == 0);
CHECK(recovered.value().transfers.uploads == 2);
float recoveredValue = 0;
std::memcpy(&recoveredValue, recovered.value().outputs[0].tensor.bytes.data(), 4);
CHECK(recoveredValue == 16);
// Session-first retirement cancels its observer and releases device resources.
{
auto second = eve::tensor::createOnnxGpuCompute();
REQUIRE(second.ok());
auto r = original.value()->runGpu({}, *second.value());
REQUIRE(r.ok());
}
// Device-first retirement clears the surviving session before destroying Vulkan.
device.reset();
eve::ModuleManager::insert("Graphics", previous);
auto stale = original.value()->runGpu({}, *gpu.value());
REQUIRE(!stale.ok());
CHECK(stale.error()->message().find("retired") != std::string::npos);
gpu.value().reset();
}
TEST_CASE("tensor.onnx.gpu.parallelCompilation") {
// Compilation has no dependency on a live Graphics device.
auto bytecode = eve::gpgpu::compileComputeSpirv("#version 450\nlayout(local_size_x=64)in;void main(){}");
REQUIRE(bytecode.ok());
REQUIRE(bytecode.value().front() == 0x07230203);
auto invalidHeader = eve::gpgpu::createComputeShader({});
REQUIRE(!invalidHeader.ok());
for (uint32_t invalid : {0u, 9u}) {
auto r = eve::tensor::createOnnxGpuCompute(invalid);
REQUIRE(!r.ok());
}
initialize();
Bytes serial;
for (uint32_t workers : {1u, 4u}) {
auto provider = eve::tensor::createOnnxGpuCompute(workers);
REQUIRE(provider.ok());
auto model = eve::tensor::OnnxModel::load(chain(4096, 2, false, true, 80));
REQUIRE(model.ok());
for (int repeat = 0; repeat < 2; ++repeat) {
auto r = model.value()->runGpu({}, *provider.value());
REQUIRE(r.ok());
CHECK(r.value().dispatches == 80);
CHECK(r.value().transfers.shaderCompilations == (repeat ? 0 : 4));
CHECK(r.value().transfers.compilerPeakWorkers <= workers);
if (!repeat)
CHECK(r.value().transfers.compilerPeakWorkers >= (workers == 1 ? 1 : 2));
else
CHECK(r.value().transfers.compilerPeakWorkers == 0);
const auto& raw = r.value().outputs[0].tensor.bytes;
for (size_t i = 0; i < 4096; ++i) {
float actual;
std::memcpy(&actual, raw.data() + i * 4, 4);
REQUIRE(actual == float(i + 20));
}
if (serial.empty())
serial = raw;
else
REQUIRE(raw == serial);
}
// A fresh, abandoned segment has active CPU compile jobs but no submitted
// GPU work. Retry must rebuild those programs and re-upload its weights.
auto canceled = eve::tensor::OnnxModel::load(chain(4093, 2, true, true));
REQUIRE(canceled.ok());
auto failed = canceled.value()->runGpu({}, *provider.value());
REQUIRE(!failed.ok());
const std::vector<std::string> requested{"v15alias"};
auto retried = canceled.value()->runGpu({}, *provider.value(), requested);
REQUIRE(retried.ok());
CHECK(retried.value().transfers.shaderCompilations == 4);
CHECK(retried.value().transfers.uploads == 2);
// Compilation failure travels through the future and run cleanup;
// subsequent valid work on the same session remains usable.
eve::tensor::OnnxKernel bad{"#version 450\nvoid main(){missing_function();}", {}, 4, 1};
auto rejected = provider.value()->dispatch(bad);
REQUIRE(!rejected.ok());
auto recovered = model.value()->runGpu({}, *provider.value());
REQUIRE(recovered.ok());
REQUIRE(recovered.value().outputs[0].tensor.bytes == serial);
}
}
#endif