Merge pull request #362 from EVEngine/codex/tensor-onnx-gpu-tts

feat(tensor): add quantized ONNX inference with reusable Vulkan sessions
This commit is contained in:
西风逍遥游 authored and GitHub committed 2026-09-11 01:08:16 +08:00
commit d7a9dab413
47 files changed
+5595 -98

No files matched your search

+23
View File
@@ -159,6 +159,26 @@ fp8/fp4 的 block scale 取 `maxAbs / 格式最大幅值`(e4m3=240、e2m1=6)
量化正确性由 `tensor.quant.*` 与 `tensor.llm.quantizedDtypes` 测试覆盖(含
GPU 编译路径),资产缺失时自动跳过。
## C++ 原生 ONNX 导入与量化 GPU 推理
`tensor/OnnxModel.h` 直接导入 ONNX,保留 int8/uint8 权重、仿射 scale/zero-point
及精确 int32/int64 数据。`run()` 执行 CPU 参考路径;`runGpu()` 接收
`createOnnxGpuCompute()` 返回的引擎 Gpgpu 适配器,在 Vulkan 设备线程上同步执行。
GPU 负责量化矩阵乘、卷积、LSTM 投影、浮点神经网络计算;形状、索引、控制流、
LSTM 门控仍由 CPU 处理;动态量化在 GPU 上计算,只回读少量校验标量。
GPU 错误明确返回,不会自动改用 CPU 重试。
已用原版 Kokoro v1.1 INT8 模型跑通中文语音整图,包括 Loop/If/Sequence。
接口使用 owning Result,输出不依赖模型生命周期,随机激励可指定种子。
当前 ONNX GPU 执行已使用驻留缓冲区和批量 Sequence 提交;连续 GPU 节点之间不回读,
仅在 CPU 数据依赖边界下载。应在推理循环外创建并保留 `createOnnxGpuCompute()`
返回的 GPU 会话,跨调用复用管线、权重和缓冲区池;Graphics 销毁前自动清理,
失效会话明确拒绝执行。实测同进程第二次合成约 2.60 秒,仍需优化 LSTM CPU 门控等开销。文字前处理、Squirrel
绑定及 dialogue 播放接入不在此示例范围内。
完整构建命令、模型资源、数值契约、限制和可重复语音测试见
[原生 ONNX 示例](../../../examples/tensor/onnx/README.md)。
## 常见问题
- 对 symbolic Tensor 调用 `get()`。
@@ -231,6 +251,9 @@ GPU 编译路径),资产缺失时自动跳过。
- 带 `update(dt)` 的系统应在 `eve_update` 调用;绘制方法应在 `eve_render` 调用。
- 参数约束、默认值和返回类型以对应模块头文件及 `addFunc` 绑定为准;本文 API 快查与当前源码同步生成。
- 张量支持 rank 1–6,dtype 为 float32 / int32;二元运算支持广播。
- 原生 ONNX GPU 会话使用有界 shader 编译队列,`createOnnxGpuCompute(compilerWorkers)`
可选 1–8 个 CPU 编译线程(默认 4);同源码去重,编译器上下文和管线缓存均复用。
Vulkan 管线创建与提交仍在设备线程,`runGpu` 保持同步返回;并发数应按目标构建实测选择。
- GPU 路径:`compile()` 需要已初始化的 Vulkan Graphics(先创建窗口)。Windows
上 GLSL→SPIR-V 由链接进引擎的 shaderc 静态库完成,不需要安装 glslc。
+64
View File
@@ -0,0 +1,64 @@
cmake_minimum_required(VERSION 3.21)
project(EveTensorOnnx LANGUAGES CXX)
get_filename_component(EVE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE)
set(ZEROERR_SOURCE "${EVE_ROOT}/external/zeroerr" CACHE PATH "Initialized zeroerr source directory")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(eve_tensor_onnx STATIC
"${EVE_ROOT}/src/modules/tensor/AffineQuant.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxReader.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxModel.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxRuntime.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxExecute.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxQuant.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxGpuKernels.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxShapeOps.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxIndexOps.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxNumeric.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxNeural.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxMisc.cpp"
"${EVE_ROOT}/src/modules/tensor/OnnxLstm.cpp"
"${EVE_ROOT}/src/modules/tensor/Tensor.cpp"
"${EVE_ROOT}/src/modules/tensor/CpuKernels.cpp"
"${EVE_ROOT}/src/engine/common/Exception.cpp"
"${EVE_ROOT}/src/engine/common/RenderTrace.cpp")
target_include_directories(eve_tensor_onnx PUBLIC "${EVE_ROOT}/src/modules" "${EVE_ROOT}/src/engine" "${ZEROERR_SOURCE}/include")
if(MSVC)
target_compile_options(eve_tensor_onnx PUBLIC /utf-8 /EHsc)
endif()
add_executable(onnx_probe main.cpp)
target_link_libraries(onnx_probe PRIVATE eve_tensor_onnx)
option(EVE_ONNX_TESTS "Build focused tensor ONNX contract tests" ON)
if(EVE_ONNX_TESTS)
set(BUILD_TEST OFF CACHE BOOL "" FORCE)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory("${ZEROERR_SOURCE}" zeroerr)
target_link_libraries(eve_tensor_onnx PUBLIC zeroerr)
add_executable(onnx_contracts "${EVE_ROOT}/test/tensor_onnx.cpp")
target_link_libraries(onnx_contracts PRIVATE eve_tensor_onnx zeroerr)
enable_testing()
foreach(case roundingAndRange exactAccumulators convolutionValidation)
add_test(NAME "tensor.onnx.affine.${case}" COMMAND onnx_contracts "--testcase=tensor.onnx.affine.${case}")
endforeach()
add_test(NAME tensor.onnx.import.transactionAndDiagnostics COMMAND onnx_contracts "--testcase=tensor.onnx.import.transactionAndDiagnostics")
add_test(NAME tensor.onnx.storage.aliasAndCpuMutation COMMAND onnx_contracts "--testcase=tensor.onnx.storage.aliasAndCpuMutation")
add_test(NAME tensor.onnx.gpu.failureAndScope COMMAND onnx_contracts "--testcase=tensor.onnx.gpu.failureAndScope")
add_test(NAME tensor.onnx.import.owningOutputs COMMAND onnx_contracts "--testcase=tensor.onnx.import.owningOutputs")
endif()
if(NOT EVE_ONNX_TESTS)
target_compile_definitions(eve_tensor_onnx PRIVATE ZEROERR_NO_ASSERT)
target_compile_definitions(onnx_probe PRIVATE ZEROERR_NO_ASSERT)
endif()
option(EVE_ONNX_VULKAN_PROBE "Enable standalone hardware parity provider (Vulkan SDK + shaderc)" OFF)
if(EVE_ONNX_VULKAN_PROBE)
find_package(Vulkan REQUIRED)
find_library(ONNX_SHADERC NAMES shaderc_shared HINTS "$ENV{VULKAN_SDK}/Lib" REQUIRED)
target_sources(onnx_probe PRIVATE VulkanProbeCompute.cpp)
target_compile_definitions(onnx_probe PRIVATE EVE_ONNX_VULKAN_PROBE)
target_link_libraries(onnx_probe PRIVATE Vulkan::Vulkan "${ONNX_SHADERC}")
if(WIN32)
add_custom_command(TARGET onnx_probe POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "$ENV{VULKAN_SDK}/Bin/shaderc_shared.dll" "$<TARGET_FILE_DIR:onnx_probe>")
endif()
endif()
+223
View File
@@ -0,0 +1,223 @@
# Native quantized ONNX inference
Direct ONNX execution in EVEngine's tensor module, without ONNX Runtime, protobuf
or Python at runtime. Original INT8 weights remain packed. The existing block
quantization format is separate from ONNX affine quantization.
## Engine GPU path
After configuring the engine with tensor enabled:
```powershell
cmake/with-msvc.cmd cmake --build build/win32-debug --target onnx_engine_probe --parallel 4
$env:EVE_ONNX_GPU='1'
build/win32-debug/test/onnx_engine_probe.exe model.int8.onnx audio audio.f32 0,73,127,172,88,108,169,4,0 style.f32
```
The probe initializes headless Graphics and calls `createOnnxGpuCompute()`. It also
checks device-unavailable behavior before initialization. `style.f32` contains 256
little-endian floats; providing it also supplies speed=1. The IDs encode “你好。”.
The current minimal host profile also needs font and devtools to link its closure:
`-DEVENGINE_PROFILE=minimal -DEVENGINE_MODULE_tensor=ON -DEVENGINE_MODULE_font=ON
-DEVENGINE_MODULE_devtools=ON`. No manifest or default profile is changed here.
C++ callers check Results from `OnnxModel::load`, `createOnnxGpuCompute` and
`model->runGpu(feeds, *compute, requestedNames)`. The result owns `outputs` and
reports successful `dispatches` plus transfer, submission and allocation/reuse counters. `run(feeds, requestedNames)` selects CPU for
reference execution. Empty requestedNames selects all graph outputs.
GPU kernels cover integer MatMul/Conv and LSTM projections, FP32 MatMul/ConvTranspose,
broadcast arithmetic, activations, Softmax, sum/mean reductions, InstanceNormalization,
Resize and CumSum. Dynamic quantization reduces range on GPU and reads only three
validation scalars; packed activation generation stays on GPU. Shape/index/sequence/control
operations, padding, other quantization variants, RNG and LSTM gates stay on CPU. GPU errors never trigger an automatic CPU retry.
The GLSL adapter explicitly rejects WebGPU.
The adapter reuses the existing `gpgpu::Sequence` recording and `submitAsync`/`wait`
mechanism used by tensor GPU execution. Consecutive GPU nodes share immutable device
buffers. Identity/reshape aliases do not transfer data. CPU reads materialize host bytes;
CPU writes detach shared storage. Packed initializers and recurrent weights are cached
by owning storage identity across runs for immutable model initializers. Dead device allocations return to a size-bounded
reuse pool. A batch flush occurs at a CPU-read boundary or after a bounded recording
backlog, with no download at a backlog flush. The public runGpu call remains synchronous.
The static `GpuProgram` compiler is not an ONNX control-flow executor; ONNX uses the same
Gpgpu buffers/sequences directly while retaining dynamic Loop/If scheduling and affine
integer semantics. Device allocations are bounded to 512 MiB, with a 128 MiB host-input
cache and size-compatible buffer reuse. All referenced buffers survive queued work.
Retain the provider returned by `createOnnxGpuCompute()` outside the dialogue/inference
loop and pass it to every `runGpu` call. It owns a reusable compiled GPU session:
shader pipelines (up to 1024 variants), model initializers (up to 128 MiB) and device
buffer pools (up to 512 MiB) persist across calls. Shader cache keys include the complete
generated source, including shapes, integer interpretation and LSTM direction offsets.
A different shape compiles only missing variants. Dynamic activations remain per-call.
Failed calls discard pending uploads before allowing cache reuse; compiled shaders stay.
The session subscribes to `Graphics::onResourcesRetiring`: resources are freed before
Vulkan device teardown even if the session outlives Graphics. A retired session rejects
GPU work; construct a new one for the new device. Destroying the session first cancels
the subscription. All of this is device-thread-only. Feeds are borrowed during the call;
outputs outlive the model and provider. Use the device's owning thread without
concurrent provider calls. Independent CPU runs are thread-safe. No global RNG,
background tasks, script callbacks or locks around compute calls are introduced.
## Standalone build and tests
```powershell
cmake/with-msvc.cmd cmake -S examples/tensor/onnx -B build/tensor-onnx -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake/with-msvc.cmd cmake --build build/tensor-onnx --parallel 4
ctest --test-dir build/tensor-onnx --output-on-failure
build/tensor-onnx/onnx_probe.exe model.int8.onnx
```
Initialize `external/zeroerr`, or set `ZEROERR_SOURCE` to an initialized checkout.
`EVE_ONNX_TESTS=OFF` removes test linkage. The CPU path needs no Vulkan, SDL or VM.
Tests and their library deliberately use the same assertion policy.
Optional `-DEVE_ONNX_VULKAN_PROBE=ON` enables a standalone hardware parity provider
using Vulkan SDK and shaderc. It executes the same tensor-generated kernels on a
physical GPU. Production uses the engine's Gpgpu adapter instead.
## Reproduce Chinese speech
Download `model.int8.onnx`, `voices.bin` and `tokens.txt` from the
[Kokoro INT8 v1.1 package](https://huggingface.co/csukuangfj/kokoro-int8-multi-lang-v1_1/tree/main).
Tested model: 114299010 bytes, SHA256
`bda15858163726a492d02a9a727bc263551b86ac77f90812c4b30ff41d380e26`.
```powershell
python -m pip install numpy onnx onnxruntime
python scripts/run_kokoro_onnx.py --model model.int8.onnx --voices voices.bin --tokens tokens.txt --probe build/win32-debug/test/onnx_engine_probe.exe --work build/kokoro-gpu --gpu
```
The utility prepares explicit phonemes and zf_001 voice data, invokes native inference,
and writes `audio.wav`, `native.log` and `result.json`. It does not transform weights.
Style selection follows [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx/blob/master/sherpa-onnx/csrc/offline-tts-kokoro-model.cc):
index by phoneme length excluding boundary zeros. This is not yet a text normalizer,
phonemizer or dialogue playback integration.
## Import and numerical contract
- IR 3–10, default opset 13–17, Microsoft opset 1; FP32, int8/uint8, int32/int64,
bool and internal sequences. External/sparse tensor data and functions are rejected.
- Bounded parsing checks truncation, sizes, duplicates and lexical captures. Nested
Loop/If bodies are parsed; scheduling includes implicit captured values, including
Kokoro captures appearing later in the serialized main graph.
- Unknown operators are reported and required unknown nodes fail before dispatch.
This bounded executor is not a complete ONNX schema validator.
- Affine APIs reject nonfinite values. The ONNX DynamicQuantizeLinear bridge matches
ORT CPU for NaN: exclude it from range estimation and emit byte code zero. Kokoro's
exported STFT phase graph produces 0/0 at zero-amplitude bins. Infinity remains an
error in quantization. Rounding is ties-to-even.
- GPU integer accumulation is exact int32, with a conservative term-count overflow
bound. CPU checks actual int64 sums before narrowing. Per-axis dequantization and
forward/reverse/bidirectional quantized LSTM are supported.
- `OnnxRunOptions.seed` controls per-node RNG streams. Floating CPU/GPU results use
tolerances. `requireFinite` is a strict diagnostic that rejects intentional
intermediate infinity/NaN; keep it false for Kokoro.
- Specialized boundaries: integer MatMul uses rank>=2 A/rank-2 B and scalar zeros;
Conv is 1D/2D NOTSET padding; ConvTranspose is 1D; nearest Resize uses
asymmetric/floor coordinates; linear Resize interpolates the final axis; Loop
supports carried tensors/sequences without scan outputs. Other variants fail.
- Limits: 512 MiB model, 100000 recursive nodes, depth 16, rank 6, 128M elements per
tensor, 10000 loop iterations, 1M execution steps. Live owned intermediates and
copied outputs are bounded to 512 MiB; model/feed data and temporary workspaces
are additional. GPU work is bounded to 65535 groups of 64.
## Validation and performance
```powershell
python scripts/test_tensor_onnx.py --probe build/tensor-onnx/onnx_probe.exe --work build/onnx-cpu-fixtures --kokoro model.int8.onnx
python scripts/test_tensor_onnx.py --probe build/tensor-onnx/onnx_probe.exe --work build/onnx-gpu-fixtures --kokoro model.int8.onnx --gpu
```
Reference packages are test-only. Coverage includes exact integer results, floating
kernels, shape operations, lexical If captures, Loop/Sequence, neural resampling,
and all six original packed Kokoro LSTMs. Contract tests include malformed inputs,
owning outputs, unsupported preflight, injected GPU failure and scope cleanup.
The original model generates 26400 finite 24 kHz samples (1.1 s of audio).
The GPU implementation remains synchronous at `runGpu`; CPU boundaries include LSTM
gates, data-dependent shape/index work and quantization validation scalars.
The hardware residency contract verifies that a 16-node Add chain with aliases uses
only two input uploads, one download and one submission, on the first run. The second run performs zero uploads and zero shader compilations.
Separate hardware tests cover staging-pool growth/reordering and descriptor retirement
when unsubmitted sequences are canceled. Run them with:
```powershell
cmake/with-msvc.cmd cmake --build build/win32-debug --target onnx_gpu_contracts --parallel 4
$env:EVENGINE_VULKAN_VALIDATION='1'
$cases = 'tensor.onnx.gpu.residentChain', 'gpgpu.sequence.stagingSizeReuse', 'gpgpu.sequence.cancelRecordingRetiresDescriptors', 'tensor.onnx.gpu.sessionShapesAndLifetime', 'tensor.onnx.gpu.parallelCompilation'
foreach ($case in $cases) {
build/win32-debug/test/onnx_gpu_contracts.exe "--testcase=$case"
if ($LASTEXITCODE -ne 0) { throw "Failed: $case" }
}
```
The session now compiles different GLSL sources on a bounded CPU queue and defers
pipeline creation/dispatch recording until a host-read or segment-budget boundary.
Dispatches retain their buffers and preserve graph order. Identical pending sources
share one future; completed pipelines persist across calls. All Vulkan access stays
on the device thread. Error cleanup drains admitted CPU jobs and discards unsubmitted
commands; compiler workers never capture a device or call the render tracer.
`createOnnxGpuCompute(compilerWorkers)` accepts 1..8 workers (default 4), with at most
48 waiting compilation jobs. The probe exposes this as `EVE_ONNX_COMPILER_WORKERS`.
This is an upper bound, not a promise of a speedup: dynamic CPU boundaries limit how
many independent sources are available. Shaderc retains a compiler/options context
per thread until that thread exits. Session workers are joined at destruction.
Thread-safety follows the [shaderc C API contract](https://github.com/google/shaderc/blob/main/libshaderc/include/shaderc/shaderc.h).
Windows CMake now defines `EVE_HAS_SHADERC` on EVGpgpu itself. Previously its
INTERFACE-only definition linked the library but left the implementation launching
`glslc.exe`. A shaderc-enabled build now reports compilation failures directly and
never requires that executable. The build without the shaderc library uses an external compiler path that retains unique
temporary names through output consumption and explicitly restricts child handle
inheritance so concurrent jobs cannot lock each other's temporary files.
Measured on the same native Windows Debug build and original model/input, with a
fresh process/session (application shader cache empty; driver caches were not cleared):
- Previous synchronous compilation: first `runGpu` 35.72-36.99 s; repeat 2.60-2.64 s.
- Four external compiler workers: first 19.89 s; repeat 2.69 s.
- Four in-process workers with retained contexts: first 8.23-9.88 s; repeat 2.74-3.26 s.
- One in-process worker with retained context: first 6.81-9.82 s; repeat 2.69-3.22 s.
Each new session compiled 370 variants; every repeat compiled zero (2608 cache hits).
Audio was byte-identical across the old implementation, external/in-process compilers,
and one/four-worker runs. Thus compiler wiring/context reuse supplies the main benefit;
on this Debug workload, four workers did not consistently beat one. The final paired
run measured 9.82 s cold / 3.22 s warm with one worker, and 9.88 s / 3.26 s with four.
Timing varied between runs, so the ranges above are retained rather than selecting only the best result. Release performance is not
established. Even the warm call exceeds the 1.1 s audio duration, so this is not real-time.
Set `EVE_ONNX_PROFILE=1` for wall-time buckets. `compile_task_ms` sums durations of
worker tasks (overlapping durations, not elapsed time); `compile_wait_ms` is device-thread
blocking on futures; `pipeline_ms` is Vulkan pipeline creation on that thread.
`shader_ms` comprises the latter two. `compiler_peak_workers` reports observed concurrency.
Allocation, upload/dispatch recording, submit/wait and host readback are also reported;
these are not GPU timestamp-query kernel timings. An earlier four-worker cold measurement reported
7.75 s aggregate compile tasks, 2.07 s compile waits and 2.86 s pipeline creation.
The one-worker control measured 3.07 s aggregate tasks, 1.22 s waits and 2.65 s pipelines.
Reproduce both configurations with the same model/session in each process:
```powershell
$env:EVE_ONNX_PROFILE='1'
$env:EVE_ONNX_COMPILER_WORKERS='4' # use 1 for the single-worker control
python scripts/run_kokoro_onnx.py --model model.int8.onnx --voices voices.bin --tokens tokens.txt --probe build/win32-debug/test/onnx_engine_probe.exe --work build/kokoro-repeat --gpu --repeat 2
```
`result.json` includes per-iteration latency, compilation/cache-hit counts, peak workers
and exact repeat verification. Hardware contracts cover serial/parallel dependent chains
across submission boundaries, changed shapes/constants, cold cancellation/retry,
compilation failure/recovery, optional-device absence and both device/session destruction orders.
Architecture rules applied: checked Results, ownership and device-thread contracts,
transactional publication, optional-device present/absent tests, failure injection,
deterministic RNG and foreign-format bounds. No ECS, Link, module boundary or engine
persistence changes are introduced.
+249
View File
@@ -0,0 +1,249 @@
#include "VulkanProbeCompute.h"
#include <shaderc/shaderc.h>
#include <vulkan/vulkan.h>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <stdexcept>
namespace {
void check(VkResult r) {
if (r != VK_SUCCESS) throw std::runtime_error("Vulkan failure: " + std::to_string(r));
}
class VulkanCompute final : public eve::tensor::OnnxCompute {
size_t dispatches = 0;
VkInstance instance = VK_NULL_HANDLE;
VkPhysicalDevice physical = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
VkQueue queue = VK_NULL_HANDLE;
VkCommandPool commands = VK_NULL_HANDLE;
VkPhysicalDeviceMemoryProperties memory{};
struct Resources {
VkDevice device;
VkCommandPool pool;
std::vector<VkBuffer> buffers;
std::vector<VkDeviceMemory> memories;
VkShaderModule shader = VK_NULL_HANDLE;
VkDescriptorSetLayout setLayout = VK_NULL_HANDLE;
VkPipelineLayout layout = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
VkDescriptorPool descriptors = VK_NULL_HANDLE;
VkCommandBuffer command = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
~Resources() {
if (fence) vkDestroyFence(device, fence, nullptr);
if (command) vkFreeCommandBuffers(device, pool, 1, &command);
if (descriptors) vkDestroyDescriptorPool(device, descriptors, nullptr);
if (pipeline) vkDestroyPipeline(device, pipeline, nullptr);
if (layout) vkDestroyPipelineLayout(device, layout, nullptr);
if (setLayout) vkDestroyDescriptorSetLayout(device, setLayout, nullptr);
if (shader) vkDestroyShaderModule(device, shader, nullptr);
for (auto b : buffers) vkDestroyBuffer(device, b, nullptr);
for (auto m : memories) vkFreeMemory(device, m, nullptr);
}
};
public:
~VulkanCompute() override {
if (commands) vkDestroyCommandPool(device, commands, nullptr);
if (device) vkDestroyDevice(device, nullptr);
if (instance) vkDestroyInstance(instance, nullptr);
}
void initialize() {
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.pApplicationName = "EVEngine ONNX GPU parity";
app.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo ci{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
ci.pApplicationInfo = &app;
check(vkCreateInstance(&ci, nullptr, &instance));
uint32_t count = 0;
check(vkEnumeratePhysicalDevices(instance, &count, nullptr));
std::vector<VkPhysicalDevice> devices(count);
check(vkEnumeratePhysicalDevices(instance, &count, devices.data()));
uint32_t family = UINT32_MAX;
for (auto d : devices) {
VkPhysicalDeviceProperties p;
vkGetPhysicalDeviceProperties(d, &p);
if (p.deviceType != VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU &&
p.deviceType != VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU)
continue;
uint32_t n = 0;
vkGetPhysicalDeviceQueueFamilyProperties(d, &n, nullptr);
std::vector<VkQueueFamilyProperties> qs(n);
vkGetPhysicalDeviceQueueFamilyProperties(d, &n, qs.data());
for (uint32_t i = 0; i < n; ++i)
if (qs[i].queueFlags & VK_QUEUE_COMPUTE_BIT) {
physical = d;
family = i;
break;
}
if (physical) {
std::cout << "ONNX_GPU_DEVICE " << p.deviceName << '\n';
break;
}
}
if (!physical) throw std::runtime_error("No hardware Vulkan compute device");
vkGetPhysicalDeviceMemoryProperties(physical, &memory);
float priority = 1;
VkDeviceQueueCreateInfo q{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
q.queueFamilyIndex = family;
q.queueCount = 1;
q.pQueuePriorities = &priority;
VkDeviceCreateInfo dc{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
dc.queueCreateInfoCount = 1;
dc.pQueueCreateInfos = &q;
check(vkCreateDevice(physical, &dc, nullptr, &device));
vkGetDeviceQueue(device, family, 0, &queue);
VkCommandPoolCreateInfo pc{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pc.queueFamilyIndex = family;
check(vkCreateCommandPool(device, &pc, nullptr, &commands));
}
eve::Result<std::vector<uint8_t>> dispatch(const eve::tensor::OnnxKernel& k) override {
try {
Resources r{device, commands};
if (k.inputs.size() >= 8 || !k.outputBytes || !k.workItems || uint64_t(k.workItems) + 63 > 65535u * 64u)
throw std::runtime_error("Invalid probe kernel extent");
const uint32_t n = static_cast<uint32_t>(k.inputs.size() + 1);
std::vector<VkDescriptorSetLayoutBinding> bindings(n);
std::vector<VkDescriptorBufferInfo> infos(n);
for (uint32_t i = 0; i < n; ++i) {
const size_t bytes = i + 1 == n ? k.outputBytes : k.inputs[i].size();
VkBufferCreateInfo bc{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bc.size = std::max(size_t(4), (bytes + 3) & ~size_t(3));
bc.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
VkBuffer b;
check(vkCreateBuffer(device, &bc, nullptr, &b));
r.buffers.push_back(b);
VkMemoryRequirements req;
vkGetBufferMemoryRequirements(device, b, &req);
uint32_t type = UINT32_MAX;
for (uint32_t j = 0; j < memory.memoryTypeCount; ++j)
if ((req.memoryTypeBits & (1u << j)) &&
(memory.memoryTypes[j].propertyFlags &
(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) ==
(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) {
type = j;
break;
}
if (type == UINT32_MAX) throw std::runtime_error("No coherent host-visible Vulkan memory");
VkMemoryAllocateInfo ma{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
ma.allocationSize = req.size;
ma.memoryTypeIndex = type;
VkDeviceMemory m;
check(vkAllocateMemory(device, &ma, nullptr, &m));
r.memories.push_back(m);
check(vkBindBufferMemory(device, b, m, 0));
void* mapped;
check(vkMapMemory(device, m, 0, bc.size, 0, &mapped));
std::memset(mapped, 0, bc.size);
if (i + 1 < n && bytes) std::memcpy(mapped, k.inputs[i].data(), bytes);
vkUnmapMemory(device, m);
bindings[i] = {i, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
infos[i] = {b, 0, bc.size};
}
auto compiler = shaderc_compiler_initialize();
if (!compiler) throw std::runtime_error("shaderc initialization failed");
auto compiled = shaderc_compile_into_spv(compiler, k.source.data(), k.source.size(), shaderc_compute_shader,
"onnx.comp", "main", nullptr);
shaderc_compiler_release(compiler);
if (!compiled) throw std::runtime_error("shaderc compilation failed");
if (shaderc_result_get_compilation_status(compiled) != shaderc_compilation_status_success) {
std::string message = shaderc_result_get_error_message(compiled);
shaderc_result_release(compiled);
throw std::runtime_error(message);
}
std::vector<uint32_t> spv(shaderc_result_get_length(compiled) / 4);
std::memcpy(spv.data(), shaderc_result_get_bytes(compiled), spv.size() * 4);
shaderc_result_release(compiled);
VkShaderModuleCreateInfo sm{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
sm.codeSize = spv.size() * 4;
sm.pCode = spv.data();
check(vkCreateShaderModule(device, &sm, nullptr, &r.shader));
VkDescriptorSetLayoutCreateInfo sl{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
sl.bindingCount = n;
sl.pBindings = bindings.data();
check(vkCreateDescriptorSetLayout(device, &sl, nullptr, &r.setLayout));
VkPipelineLayoutCreateInfo pl{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pl.setLayoutCount = 1;
pl.pSetLayouts = &r.setLayout;
check(vkCreatePipelineLayout(device, &pl, nullptr, &r.layout));
VkComputePipelineCreateInfo cp{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
cp.layout = r.layout;
cp.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
cp.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
cp.stage.module = r.shader;
cp.stage.pName = "main";
check(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &cp, nullptr, &r.pipeline));
VkDescriptorPoolSize ps{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, n};
VkDescriptorPoolCreateInfo dp{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
dp.maxSets = 1;
dp.poolSizeCount = 1;
dp.pPoolSizes = &ps;
check(vkCreateDescriptorPool(device, &dp, nullptr, &r.descriptors));
VkDescriptorSetAllocateInfo da{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
da.descriptorPool = r.descriptors;
da.descriptorSetCount = 1;
da.pSetLayouts = &r.setLayout;
VkDescriptorSet set;
check(vkAllocateDescriptorSets(device, &da, &set));
std::vector<VkWriteDescriptorSet> writes(n);
for (uint32_t i = 0; i < n; ++i) {
writes[i] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
writes[i].dstSet = set;
writes[i].dstBinding = i;
writes[i].descriptorCount = 1;
writes[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
writes[i].pBufferInfo = &infos[i];
}
vkUpdateDescriptorSets(device, n, writes.data(), 0, nullptr);
VkCommandBufferAllocateInfo ca{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
ca.commandPool = commands;
ca.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
ca.commandBufferCount = 1;
check(vkAllocateCommandBuffers(device, &ca, &r.command));
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
check(vkBeginCommandBuffer(r.command, &begin));
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(r.command, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1,
&barrier, 0, nullptr, 0, nullptr);
vkCmdBindPipeline(r.command, VK_PIPELINE_BIND_POINT_COMPUTE, r.pipeline);
vkCmdBindDescriptorSets(r.command, VK_PIPELINE_BIND_POINT_COMPUTE, r.layout, 0, 1, &set, 0, nullptr);
vkCmdDispatch(r.command, (k.workItems + 63) / 64, 1, 1);
barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
vkCmdPipelineBarrier(r.command, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1,
&barrier, 0, nullptr, 0, nullptr);
check(vkEndCommandBuffer(r.command));
VkFenceCreateInfo fc{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
check(vkCreateFence(device, &fc, nullptr, &r.fence));
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &r.command;
check(vkQueueSubmit(queue, 1, &submit, r.fence));
check(vkWaitForFences(device, 1, &r.fence, VK_TRUE, UINT64_MAX));
std::vector<uint8_t> result(k.outputBytes);
void* mapped;
check(vkMapMemory(device, r.memories.back(), 0, k.outputBytes, 0, &mapped));
std::memcpy(result.data(), mapped, k.outputBytes);
vkUnmapMemory(device, r.memories.back());
if (++dispatches % 100 == 0) std::cout << "GPU_DISPATCHES " << dispatches << std::endl;
return eve::Result<std::vector<uint8_t>>::success(std::move(result));
} catch (const std::exception& e) {
return eve::Result<std::vector<uint8_t>>::failure(
eve::Diagnostic::error(eve::DiagnosticCode::Failed, e.what()));
}
}
};
} // namespace
eve::Result<std::unique_ptr<eve::tensor::OnnxCompute>> createVulkanProbeCompute() {
try {
auto p = std::make_unique<VulkanCompute>();
p->initialize();
return eve::Result<std::unique_ptr<eve::tensor::OnnxCompute>>::success(std::move(p));
} catch (const std::exception& e) {
return eve::Result<std::unique_ptr<eve::tensor::OnnxCompute>>::failure(
eve::Diagnostic::error(eve::DiagnosticCode::Failed, e.what()));
}
}
@@ -0,0 +1,4 @@
#pragma once
#include "tensor/OnnxCompute.h"
/** @brief Standalone hardware test provider; production uses createOnnxGpuCompute(). */
[[nodiscard]] eve::Result<std::unique_ptr<eve::tensor::OnnxCompute>> createVulkanProbeCompute();
+156
View File
@@ -0,0 +1,156 @@
#include <chrono>
#include <cstdlib>
#include "tensor/OnnxModel.h"
#ifdef EVE_ONNX_ENGINE_PROVIDER
#include "common/CrashHandler.h"
#include "graphics/Graphics.h"
#include "tensor/OnnxCompute.h"
#elif defined(EVE_ONNX_VULKAN_PROBE)
#include "VulkanProbeCompute.h"
#endif
#include <cstring>
#include <fstream>
#include <iostream>
#include <sstream>
int main(int argc, char** argv) {
#ifdef EVE_ONNX_ENGINE_PROVIDER
eve::installCrashHandler();
#endif
std::cout.setf(std::ios::unitbuf);
if (argc < 2) {
std::cerr << "Usage: onnx_probe MODEL [OUTPUT_NAME RAW_OUTPUT_FILE [COMMA_SEPARATED_TOKEN_IDS]]\n";
return 2;
}
std::ifstream file(argv[1], std::ios::binary | std::ios::ate);
if (!file || file.tellg() <= 0 || file.tellg() > 512 * 1024 * 1024) return 2;
std::vector<uint8_t> bytes(static_cast<size_t>(file.tellg()));
file.seekg(0);
if (!file.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) return 2;
const auto importStart = std::chrono::steady_clock::now();
auto imported = eve::tensor::OnnxModel::load(bytes);
std::cout << "ONNX_IMPORT_TIME ms="
<< std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - importStart).count()
<< '\n';
if (!imported.ok()) {
std::cerr << imported.error()->message() << '\n';
return 1;
}
auto model = std::move(imported.value());
const auto info = model->info();
std::cout << "ONNX_IMPORT nodes=" << info.nodeCount << " packed_bytes=" << info.initializerBytes
<< " unsupported_nodes=" << info.unsupportedNodes.size() << '\n';
if (argc < 4) {
for (const auto& node : info.unsupportedNodes) std::cout << node << '\n';
return 0;
}
std::vector<eve::tensor::OnnxNamedTensor> feeds;
if (argc > 4) {
std::vector<int64_t> ids;
std::istringstream text(argv[4]);
std::string item;
try {
while (std::getline(text, item, ',')) ids.push_back(std::stoll(item));
} catch (const std::exception&) {
return 2;
}
eve::tensor::OnnxTensor tokens{eve::tensor::OnnxElement::Int64,
{1, static_cast<int64_t>(ids.size())},
std::vector<uint8_t>(ids.size() * 8)};
if (!ids.empty()) std::memcpy(tokens.bytes.data(), ids.data(), tokens.bytes.size());
feeds.push_back({"tokens", std::move(tokens)});
}
if (argc > 5) {
eve::tensor::OnnxTensor style{eve::tensor::OnnxElement::Float32, {1, 256}, std::vector<uint8_t>(1024)};
std::ifstream voice(argv[5], std::ios::binary | std::ios::ate);
if (!voice || voice.tellg() != 1024) {
std::cerr << "Style file must contain 256 float32 values\n";
return 2;
}
voice.seekg(0);
if (!voice.read(reinterpret_cast<char*>(style.bytes.data()), 1024)) return 2;
feeds.push_back({"style", std::move(style)});
eve::tensor::OnnxTensor speed{eve::tensor::OnnxElement::Float32, {1}, std::vector<uint8_t>(4)};
const float rate = 1;
std::memcpy(speed.bytes.data(), &rate, 4);
feeds.push_back({"speed", std::move(speed)});
}
const std::vector<std::string> outputs{argv[2]};
size_t dispatches = 0;
const bool gpu = std::getenv("EVE_ONNX_GPU") && std::string(std::getenv("EVE_ONNX_GPU")) == "1";
eve::tensor::OnnxRunOptions options;
options.requireFinite = std::getenv("EVE_ONNX_CHECK_FINITE") != nullptr;
auto execute = [&]() -> eve::Result<std::vector<eve::tensor::OnnxNamedTensor>> {
if (!gpu) return model->run(feeds, outputs, options);
#if defined(EVE_ONNX_VULKAN_PROBE) || defined(EVE_ONNX_ENGINE_PROVIDER)
#ifdef EVE_ONNX_ENGINE_PROVIDER
auto unavailable = eve::tensor::createOnnxGpuCompute();
if (unavailable.ok())
return eve::Result<std::vector<eve::tensor::OnnxNamedTensor>>::failure(eve::Diagnostic::error(
eve::DiagnosticCode::Failed, "Unexpected device before headless initialization"));
std::cout << "ONNX_GPU_ABSENT_PASS\n";
auto* graphics = eve::graphics::Graphics::create();
graphics->initHeadless(64, 64);
uint32_t compilerWorkers = 4;
if (const auto* value = std::getenv("EVE_ONNX_COMPILER_WORKERS"))
compilerWorkers = static_cast<uint32_t>(std::stoul(value));
auto provider = eve::tensor::createOnnxGpuCompute(compilerWorkers);
#else
auto provider = createVulkanProbeCompute();
#endif
if (!provider.ok()) return eve::Result<std::vector<eve::tensor::OnnxNamedTensor>>::failure(provider.status());
auto runOnce = [&](int iteration) {
const auto runStart = std::chrono::steady_clock::now();
auto result = model->runGpu(feeds, *provider.value(), outputs, options);
std::cout << "ONNX_ITERATION index=" << iteration << " ms="
<< std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - runStart).count();
if (result.ok())
std::cout << " compilations=" << result.value().transfers.shaderCompilations
<< " compiler_peak_workers=" << result.value().transfers.compilerPeakWorkers
<< " cache_hits=" << result.value().transfers.shaderCacheHits
<< " upload_bytes=" << result.value().transfers.uploadedBytes;
std::cout << '\n';
return result;
};
auto result = runOnce(0);
if (const auto* repeat = std::getenv("EVE_ONNX_REPEAT"); repeat && std::string(repeat) == "2") {
if (!result.ok()) return eve::Result<std::vector<eve::tensor::OnnxNamedTensor>>::failure(result.status());
auto previous = result.value().outputs;
result = runOnce(1);
if (result.ok()) {
if (previous.size() != result.value().outputs.size())
return eve::Result<std::vector<eve::tensor::OnnxNamedTensor>>::failure(
eve::Diagnostic::error(eve::DiagnosticCode::Failed, "Repeat output count mismatch"));
for (size_t i = 0; i < previous.size(); ++i)
if (previous[i].tensor.bytes != result.value().outputs[i].tensor.bytes)
return eve::Result<std::vector<eve::tensor::OnnxNamedTensor>>::failure(
eve::Diagnostic::error(eve::DiagnosticCode::Failed, "Repeat output bytes differ"));
std::cout << "ONNX_REPEAT_IDENTICAL_PASS\n";
}
}
if (!result.ok()) return eve::Result<std::vector<eve::tensor::OnnxNamedTensor>>::failure(result.status());
dispatches = result.value().dispatches;
const auto& t = result.value().transfers;
std::cout << "ONNX_TRANSFERS uploads=" << t.uploads << " downloads=" << t.downloads
<< " submissions=" << t.submissions << " upload_bytes=" << t.uploadedBytes
<< " download_bytes=" << t.downloadedBytes << " allocations=" << t.bufferAllocations
<< " reuses=" << t.bufferReuses << " compilations=" << t.shaderCompilations
<< " cache_hits=" << t.shaderCacheHits << '\n';
return eve::Result<std::vector<eve::tensor::OnnxNamedTensor>>::success(std::move(result.value().outputs));
#else
return eve::Result<std::vector<eve::tensor::OnnxNamedTensor>>::failure(eve::Diagnostic::error(
eve::DiagnosticCode::Unsupported, "Build EVE_ONNX_VULKAN_PROBE to enable GPU parity"));
#endif
};
auto generated = execute();
if (!generated.ok()) {
std::cerr << generated.error()->path() << ": " << generated.error()->message() << '\n';
return 1;
}
const auto& output = generated.value().front().tensor;
std::ofstream target(argv[3], std::ios::binary);
if (!target.write(reinterpret_cast<const char*>(output.bytes.data()), output.bytes.size())) return 2;
std::cout << "ONNX_RUN_PASS backend=" << (gpu ? "vulkan_quantized" : "engine_cpu") << " dispatches=" << dispatches
<< " bytes=" << output.bytes.size() << '\n';
}
+1 -1
View File
@@ -348,7 +348,7 @@ POINTER_API = re.compile(
r"^\s*(?:(?:\[\[nodiscard(?:\([^]]*\))?\]\]\s*)?"
r"(?:static|virtual|inline|constexpr|explicit|const)\s+)*"
r"(?:[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*(?:\s*<[^;{}()]+>)?\s*)?\*\s*"
r"(?:[A-Za-z_]\w*)?\s*(?:\(|;|=)"
r"[A-Za-z_]\w*\s*(?:\(|;|=)"
)
# A bool query is a legitimate API shape when its name follows the project's
# query vocabulary. Keep this list deliberately lexical: this checker does
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Exercise native Kokoro INT8 inference with explicit phonemes and a selected voice.
Python/ONNX/NumPy prepare test inputs and WAV output only. Inference runs in the
native probe, without ONNX Runtime. Default phonemes say Chinese 'ni hao'.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
import time
import wave
import numpy as np
import onnx
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--model', type=Path, required=True)
parser.add_argument('--voices', type=Path, required=True)
parser.add_argument('--tokens', type=Path, required=True)
parser.add_argument('--probe', type=Path, required=True)
parser.add_argument('--work', type=Path, required=True)
parser.add_argument('--gpu', action='store_true')
parser.add_argument('--repeat', type=int, choices=[1, 2], default=1)
parser.add_argument('--speaker', type=int, default=3)
parser.add_argument('--phonemes', default='ㄋ ㄧ 2 ㄏ ㄠ 3 .')
args = parser.parse_args()
args.work.mkdir(parents=True, exist_ok=True)
model = onnx.load(args.model, load_external_data=False)
metadata = {entry.key: entry.value for entry in model.metadata_props}
dims = [int(d) for d in metadata['style_dim'].split(',')]
speakers = int(metadata['n_speakers'])
if dims[1:] != [1, 256] or not 0 <= args.speaker < speakers:
raise ValueError('Unsupported voice shape or speaker index')
vocab = {line.rsplit(' ', 1)[0]: int(line.rsplit(' ', 1)[1])
for line in args.tokens.read_text(encoding='utf-8').splitlines()}
phones = args.phonemes.split()
if not 0 < len(phones) < dims[0]:
raise ValueError('Phoneme count exceeds the voice table')
ids = [0] + [vocab[p] for p in phones] + [0]
voices = np.memmap(args.voices, mode='r', dtype='<f4')
if voices.size != speakers * dims[0] * dims[2]:
raise ValueError('Voice table size does not match model metadata')
# sherpa-onnx indexes style by phoneme length, excluding the two boundary zeros.
style = voices.reshape(speakers, dims[0], dims[2])[args.speaker, len(phones)]
style_path = args.work / 'style.f32'
style.tofile(style_path)
raw = args.work / 'audio.f32'
env = os.environ.copy()
env['EVE_ONNX_GPU'] = '1' if args.gpu else '0'
env.pop('EVE_ONNX_CHECK_FINITE', None)
env['EVE_ONNX_REPEAT'] = str(args.repeat)
started = time.perf_counter()
run = subprocess.run([str(args.probe.resolve()), str(args.model.resolve()), 'audio',
str(raw.resolve()), ','.join(map(str, ids)), str(style_path.resolve())],
env=env, capture_output=True, text=True, encoding='utf-8', errors='replace')
seconds = time.perf_counter() - started
(args.work / 'native.log').write_text(run.stdout + run.stderr, encoding='utf-8')
if run.returncode or 'ONNX_RUN_PASS' not in run.stdout:
raise RuntimeError('Native inference failed; see ' + str(args.work / 'native.log'))
dispatches = int(re.search(r'dispatches=(\d+)', run.stdout).group(1))
if args.gpu and dispatches == 0:
raise RuntimeError('GPU run completed without any GPU dispatch')
audio = np.fromfile(raw, '<f4')
if not audio.size or not np.isfinite(audio).all() or np.max(np.abs(audio)) == 0:
raise RuntimeError('Empty, nonfinite or silent audio output')
sample_rate = int(metadata['sample_rate'])
with wave.open(str(args.work / 'audio.wav'), 'wb') as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(sample_rate)
wav.writeframes((np.clip(audio, -1, 1) * 32767).astype('<i2').tobytes())
report = dict(schema='eve.tensor.kokoro-probe/1', model_sha256=hashlib.sha256(args.model.read_bytes()).hexdigest(),
model_bytes=args.model.stat().st_size, speaker=args.speaker, phonemes=phones,
token_ids=ids, backend='gpu' if args.gpu else 'cpu', gpu_dispatches=dispatches,
samples=int(audio.size), sample_rate=sample_rate, peak=float(np.max(np.abs(audio))),
inference_wall_seconds=seconds, audio_seconds=audio.size / sample_rate)
report['iterations'] = [{key: (float(value) if key == 'ms' else int(value))
for key, value in re.findall(r'(\w+)=([\d.]+)', line)}
for line in re.findall(r'ONNX_ITERATION ([^\r\n]+)', run.stdout)]
if args.repeat == 2 and ('ONNX_REPEAT_IDENTICAL_PASS' not in run.stdout or len(report['iterations']) != 2):
raise RuntimeError('Missing repeated-inference equivalence proof')
transfers = re.search(r'ONNX_TRANSFERS ([^\r\n]+)', run.stdout)
if transfers:
report['transfers'] = {key: int(value) for key, value in re.findall(r'(\w+)=(\d+)', transfers.group(1))}
(args.work / 'result.json').write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')
print('KOKORO_ONNX_PASS', args.work / 'audio.wav', f'{dispatches} GPU dispatches, {seconds:.3f}s wall time')
if __name__ == '__main__':
main()
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""Compare the native tensor ONNX probe against ONNX ReferenceEvaluator/ORT.
Python and ONNX Runtime are test-only dependencies; the engine never links them.
Usage: python scripts/test_tensor_onnx.py --probe PATH --work PATH [--kokoro PATH]
"""
import argparse
import os
import re
import json
from pathlib import Path
import subprocess
import numpy as np
import onnx
from onnx import helper as h, numpy_helper as nh
from onnx.reference import ReferenceEvaluator
import onnxruntime as ort
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--probe', type=Path, required=True)
parser.add_argument('--work', type=Path, required=True)
parser.add_argument('--kokoro', type=Path)
parser.add_argument('--gpu', action='store_true')
args = parser.parse_args()
if args.gpu:
os.environ['EVE_ONNX_GPU'] = '1'
else:
os.environ.pop('EVE_ONNX_GPU', None)
args.work.mkdir(parents=True, exist_ok=True)
probe = str(args.probe.resolve())
results = []
def check(name, nodes, values, outputs, reference='ort', tolerance=1e-6):
initializers = [nh.from_array(np.asarray(v), k) for k, v in values.items()]
graph = h.make_graph(nodes, name, [], [h.make_empty_tensor_value_info(o) for o in outputs], initializers)
model = h.make_model(graph, ir_version=7, opset_imports=[h.make_opsetid('', 14), h.make_opsetid('com.microsoft', 1)])
path = args.work / (name + '.onnx')
onnx.save(model, path)
if reference == 'ort':
options = ort.SessionOptions()
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL
expected = ort.InferenceSession(model.SerializeToString(), options, providers=['CPUExecutionProvider']).run(outputs, {})
else:
expected = ReferenceEvaluator(model).run(outputs, {})
for output, ref in zip(outputs, expected):
raw = args.work / (name + '-' + output.replace('/', '_') + '.raw')
result = subprocess.run([probe, str(path), output, str(raw)], capture_output=True, text=True)
if result.returncode:
raise RuntimeError(name + ': ' + result.stdout + result.stderr)
actual = np.fromfile(raw, dtype=ref.dtype).reshape(ref.shape)
error = float(np.max(np.abs(actual.astype(np.float64) - ref.astype(np.float64)))) if ref.size else 0
if np.issubdtype(ref.dtype, np.integer):
np.testing.assert_array_equal(actual, ref)
else:
np.testing.assert_allclose(actual, ref, atol=tolerance, rtol=tolerance)
dispatches=int(re.search(r'dispatches=(\d+)',result.stdout).group(1))
if args.gpu and any(n.op_type in ['MatMulInteger','ConvInteger','DynamicQuantizeLSTM','MatMul','Add','Softmax','ReduceMean'] for n in nodes):
assert dispatches>0, (name,result.stdout)
results.append(dict(gpu_dispatches=dispatches, backend='gpu' if args.gpu else 'cpu', case=name, output=output, max_abs_error=error, tolerance=tolerance, reference=reference))
print(name, output, 'PASS', error)
for i, x in enumerate([[-1., 0, 1], [0., 0], [2., 3.], [-3., -2.], [-0.01, 0.03], [np.nan,1.,-1.], [np.nan,np.nan]]):
check('dynamic' + str(i), [h.make_node('DynamicQuantizeLinear', ['x'], ['q','s','z'])],
{'x': np.array(x, np.float32)}, ['q','s','z'], reference='ort')
for sign in [False, True]:
dtype = np.int8 if sign else np.uint8
check('ties' + str(sign), [h.make_node('QuantizeLinear', ['x','s','z'], ['q'])],
{'x':np.array([-1000,-1.25,-0.75,-0.25,0.25,0.75,1.25,1000],np.float32),
's':np.array(0.5,np.float32),'z':np.array(0 if sign else 128,dtype)}, ['q'])
check('per_axis', [h.make_node('DequantizeLinear',['q','s','z'],['y'],axis=1)],
{'q':np.arange(24,dtype=np.uint8).reshape(2,3,4),'s':np.array([.1,.2,.3],np.float32),'z':np.array([2,5,8],np.uint8)}, ['y'])
check('integer_exact', [h.make_node('MatMulInteger',['a','b'],['y'])],
{'a':np.full((1,1025),255,np.uint8),'b':np.full((1025,1),127,np.int8)}, ['y'])
check('signed_zero', [h.make_node('MatMulInteger',['a','b','az','bz'],['y'])],
{'a':np.array([[-128,127,0]],np.int8),'b':np.array([[3,4],[5,6],[7,8]],np.uint8),'az':np.array(-3,np.int8),'bz':np.array(4,np.uint8)}, ['y'])
check('conv_group', [h.make_node('ConvInteger',['x','w','xz','wz'],['y'],group=2,pads=[1,0,0,1],dilations=[1,2])],
{'x':np.arange(2*4*4,dtype=np.uint8).reshape(1,2,4,4),'w':np.array(range(8),np.uint8).reshape(2,1,2,2),'xz':np.array(7,np.uint8),'wz':np.array(2,np.uint8)}, ['y'])
check('conv1d', [h.make_node('ConvInteger',['x','w'],['y'],pads=[1,2],strides=[2])],
{'x':np.arange(9,dtype=np.uint8).reshape(1,1,9),'w':np.array([1,2,3],np.uint8).reshape(1,1,3)}, ['y'])
check('integer64_shape', [h.make_node('Gather',['x','i'],['y'])],
{'x':np.array([2**60+1,-2**60-3],np.int64),'i':np.array([1,0],np.int64)}, ['y'])
check('reshape_transpose', [h.make_node('Reshape',['x','s'],['r']), h.make_node('Transpose',['r'],['y'],perm=[1,0])],
{'x':np.arange(12,dtype=np.float32),'s':np.array([3,-1],np.int64)}, ['y'])
check('slice_reverse',[h.make_node('Slice',['x','s','e','a','st'],['y'])],
{'x':np.arange(24,dtype=np.int64).reshape(2,3,4),'s':np.array([-1,-1],np.int64),'e':np.array([-100,-100],np.int64),'a':np.array([0,2],np.int64),'st':np.array([-1,-2],np.int64)},['y'])
check('broadcast_empty',[h.make_node('Expand',['x','s'],['y'])],{'x':np.ones((1,3),np.float32),'s':np.array([0,3],np.int64)},['y'])
check('concat',[h.make_node('Concat',['x','b'],['y'],axis=-1)],{'x':np.arange(6,dtype=np.int64).reshape(2,3),'b':np.array([[20],[30]],np.int64)},['y'])
check('where',[h.make_node('Where',['c','a','b'],['y'])],{'c':np.array([[True],[False]]),'a':np.array([2**60+1,2**60+2],np.int64),'b':np.array(0,np.int64)},['y'])
check('range_reverse',[h.make_node('Range',['s','e','d'],['y'])],{'s':np.array(2**60+9,np.int64),'e':np.array(2**60,np.int64),'d':np.array(-2,np.int64)},['y'],reference='reference')
check('topk_ties',[h.make_node('TopK',['x','k'],['y','indices'],axis=0)],{'x':np.array([[2,4],[2,3],[1,5]],np.float32),'k':np.array([2],np.int64)},['y','indices'])
check('scatter_nd',[h.make_node('ScatterND',['x','i','u'],['y'])],{'x':np.zeros((3,2),np.float32),'i':np.array([[-1],[0]],np.int64),'u':np.array([[4,5],[6,7]],np.float32)},['y'])
check('reduce_product',[h.make_node('ReduceProd',['x'],['y'],axes=[0],keepdims=0)],{'x':np.array([[2,3],[4,5]],np.int64)},['y'])
check('broadcast_float',[h.make_node('Add',['a','b'],['y'])],{'a':np.arange(24,dtype=np.float32).reshape(2,3,4),'b':np.array([[.1],[.2],[.3]],np.float32)},['y'])
for name,a,b in [('batched',np.arange(24,dtype=np.float32).reshape(2,3,4)/20,np.arange(20,dtype=np.float32).reshape(4,5)/30),('vector',np.array([1.,2.,3.],np.float32),np.arange(6,dtype=np.float32).reshape(3,2)),('dot',np.array([1.,2.],np.float32),np.array([3.,4.],np.float32))]:
check('matmul_'+name,[h.make_node('MatMul',['a','b'],['y'])],{'a':a,'b':b},['y'])
check('pow_negative',[h.make_node('Pow',['x','p'],['y'])],{'x':np.array([-3.,-2.,-.1,0,2],np.float32),'p':np.array(2,np.float32)},['y'])
check('reduce_mean',[h.make_node('ReduceMean',['x'],['y'],axes=[0,2],keepdims=0)],{'x':np.arange(24,dtype=np.float32).reshape(2,3,4)/7},['y'])
check('softmax_axis',[h.make_node('Softmax',['x'],['y'],axis=1)],{'x':np.arange(24,dtype=np.float32).reshape(2,3,4)/7},['y'])
for op in ['Reciprocal','Floor','Round','Atan','LeakyRelu','Sqrt','Exp','Log','Sin','Cos','Tanh','Sigmoid']:
check('unary_'+op,[h.make_node(op,['x'],['y'])],{'x':np.array([.25,.5,1.,1.5,2.5,3.5],np.float32)},['y'],tolerance=2e-6)
check('atan_infinity',[h.make_node('Atan',['x'],['y'])],{'x':np.array([-np.inf,np.inf],np.float32)},['y'],tolerance=2e-6)
check('instance_norm',[h.make_node('InstanceNormalization',['x','s','b'],['y'],epsilon=1e-5)],{'x':np.arange(24,dtype=np.float32).reshape(2,3,4)/11,'s':np.array([1,2,3],np.float32),'b':np.array([.1,.2,.3],np.float32)},['y'],tolerance=3e-5)
check('transpose_group',[h.make_node('ConvTranspose',['x','w','b'],['y'],group=2,strides=[2],pads=[1,1],output_padding=[1])],{'x':np.arange(12,dtype=np.float32).reshape(1,4,3)/13,'w':np.arange(24,dtype=np.float32).reshape(4,2,3)/7,'b':np.arange(4,dtype=np.float32)},['y'],tolerance=3e-6)
for mode in ['nearest','linear']:
check('resize_'+mode,[h.make_node('Resize',['x','','s'],['y'],mode=mode,coordinate_transformation_mode='asymmetric' if mode=='nearest' else 'half_pixel',nearest_mode='floor')],{'x':np.arange(6,dtype=np.float32).reshape(1,2,3),'s':np.array([1,1,2.5],np.float32)},['y'],tolerance=3e-6)
for mode in ['reflect','edge','constant']:
check('pad_'+mode,[h.make_node('Pad',['x','p'],['y'],mode=mode)],{'x':np.arange(6,dtype=np.float32).reshape(2,3),'p':np.array([0,2,0,1],np.int64)},['y'])
for exclusive in [0,1]:
check('cumsum_'+str(exclusive),[h.make_node('CumSum',['x','a'],['y'],exclusive=exclusive,reverse=1)],{'x':np.arange(24,dtype=np.float32).reshape(2,3,4),'a':np.array(1,np.int64)},['y'])
check('clip',[h.make_node('Clip',['x','lo','hi'],['y'])],{'x':np.arange(6,dtype=np.float32),'lo':np.array(1.,np.float32),'hi':np.array(4.,np.float32)},['y'])
branch=h.make_graph([h.make_node('Identity',['captured'],['branch_result'])],'branch',[],[h.make_tensor_value_info('branch_result',onnx.TensorProto.FLOAT,[2])])
check('if_lexical_capture',[h.make_node('If',['condition'],['y'],then_branch=branch,else_branch=branch),h.make_node('Identity',['x'],['captured'])],{'condition':np.array(True),'x':np.array([2,3],np.float32)},['y'])
body=h.make_graph([h.make_node('Identity',['condition_in'],['condition_out']),h.make_node('SequenceInsert',['state_in','iteration'],['state_out'])],'body',
[h.make_tensor_value_info('iteration',onnx.TensorProto.INT64,[]),h.make_tensor_value_info('condition_in',onnx.TensorProto.BOOL,[]),h.make_tensor_sequence_value_info('state_in',onnx.TensorProto.INT64,[])],
[h.make_tensor_value_info('condition_out',onnx.TensorProto.BOOL,[]),h.make_tensor_sequence_value_info('state_out',onnx.TensorProto.INT64,[])])
check('loop_sequence',[h.make_node('SequenceEmpty',[],['empty'],dtype=onnx.TensorProto.INT64),h.make_node('Loop',['trip','condition','empty'],['sequence'],body=body),h.make_node('ConcatFromSequence',['sequence'],['y'],axis=0,new_axis=1)],{'trip':np.array(3,np.int64),'condition':np.array(True)},['y'])
check('split_sequence',[h.make_node('SplitToSequence',['x','split'],['seq'],axis=1),h.make_node('SequenceAt',['seq','index'],['y'])],{'x':np.arange(12,dtype=np.int64).reshape(2,6),'split':np.array([2,1,3],np.int64),'index':np.array(-1,np.int64)},['y'])
rng=np.random.default_rng(3)
for direction in ['forward','reverse','bidirectional']:
d=2 if direction=='bidirectional' else 1
values={'x':rng.normal(size=(4,2,3)).astype(np.float32),
'w':rng.integers(-60,60,(d,3,8),dtype=np.int8),'r':rng.integers(-60,60,(d,2,8),dtype=np.int8),
'b':rng.normal(0,.1,(d,16)).astype(np.float32),
'ws':np.full((d,8),.01,np.float32),'wz':np.zeros((d,8),np.int8),
'rs':np.full((d,8),.01,np.float32),'rz':np.zeros((d,8),np.int8)}
check('lstm_'+direction,[h.make_node('DynamicQuantizeLSTM',['x','w','r','b','','','','','ws','wz','rs','rz'],['y','yh','yc'],domain='com.microsoft',hidden_size=2,direction=direction)],values,['y','yh','yc'],reference='ort',tolerance=.004)
values['lengths']=np.array([4,2],np.int32)
values['h']=rng.normal(0,.1,(d,2,2)).astype(np.float32)
values['c']=rng.normal(0,.1,(d,2,2)).astype(np.float32)
check('lstm_state_'+direction,[h.make_node('DynamicQuantizeLSTM',['x','w','r','b','lengths','h','c','','ws','wz','rs','rz'],['y','yh','yc'],domain='com.microsoft',hidden_size=2,direction=direction)],values,['y','yh','yc'],reference='ort',tolerance=.004)
if args.kokoro:
model=onnx.load(args.kokoro)
initializers={x.name:x for x in model.graph.initializer}
for index, lstm in enumerate(n for n in model.graph.node if n.op_type=='DynamicQuantizeLSTM'):
# Keep the original packed weights, scales, and node attributes.
constants={k:nh.to_array(initializers[k]) for k in lstm.input if k in initializers}
weight=constants[lstm.input[1]]
constants[lstm.input[0]]=rng.normal(0,.1,(3,1,weight.shape[1])).astype(np.float32)
for i in [4,5,6,7]:
if i<len(lstm.input) and lstm.input[i] and lstm.input[i] not in constants:
lstm.input[i]=''
check('kokoro_lstm_'+str(index),[lstm],constants,list(lstm.output),reference='ort',tolerance=1e-5)
(args.work/'results.json').write_text(json.dumps(results,indent=2),encoding='utf-8')
print('TENSOR_ONNX_PARITY_PASS',len(results),'outputs')
if __name__=='__main__':
main()
@@ -54,6 +54,19 @@ class ArchitectureContractTests(unittest.TestCase):
self.assertIn("last-error-channel", codes)
self.assertIn("undocumented-raw-pointer-api", codes)
def test_multiply_assignment_is_not_a_pointer_api(self):
for text in ("n *= static_cast<size_t>(d);", "n *= count(shape);"):
with self.subTest(text=text):
lines = [contracts.SourceLine("fixture.h", 1, text)]
self.assertEqual([], contracts.lint_api_shapes(lines))
def test_named_pointer_apis_still_require_lifetime_contracts(self):
for text in ("Widget* get();", "Widget* value = makeWidget();"):
with self.subTest(text=text):
lines = [contracts.SourceLine("fixture.h", 1, text)]
codes = {finding.code for finding in contracts.lint_api_shapes(lines)}
self.assertIn("undocumented-raw-pointer-api", codes)
def test_new_link_and_system_need_catalogue_coverage(self):
metadata = {
"entries": [
+3 -1
View File
@@ -488,7 +488,9 @@ if(_has_gpgpu)
# INTERFACE: EVGpgpu is an OBJECT library; its private link deps would not
# reach the final executable, so propagate the SDK archive to consumers.
target_link_libraries(EVGpgpu INTERFACE ${EVE_SHADERC_LIB})
target_compile_definitions(EVGpgpu INTERFACE EVE_HAS_SHADERC=1)
# The implementation itself selects shaderc; INTERFACE would only define
# the macro for consumers and silently leave VulkanUtil spawning glslc.
target_compile_definitions(EVGpgpu PRIVATE EVE_HAS_SHADERC=1)
message(STATUS "Gpgpu: in-process shaderc compiler: ${EVE_SHADERC_LIB}")
else()
message(STATUS "Gpgpu: shaderc lib not found; will spawn glslc at runtime")
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "common/Result.h"
namespace eve::gpgpu {
class ComputeShader;
/** @brief Compile GLSL compute source to owning SPIR-V without accessing Graphics or a GPU.
* @param source Borrowed until return; independent calls may execute concurrently on worker threads.
* @return Owning words, compilation diagnostic, or Unsupported on WebGPU. No callbacks; each thread retains its own
* compiler context until thread exit.
*/
[[nodiscard]] Result<std::vector<uint32_t>> compileComputeSpirv(const std::string& source);
/** @brief Create an owning compute pipeline from valid SPIR-V produced by compileComputeSpirv.
* @param words Borrowed only until return; must contain a valid compute entry point named main.
* @return Owning shader or a diagnostic; Unsupported on WebGPU.
* @note Device thread only, no concurrent calls/reentrancy or Graphics retirement during the call.
* The caller must destroy the shader before the active Graphics device retires.
*/
[[nodiscard]] Result<std::unique_ptr<ComputeShader>> createComputeShader(const std::vector<uint32_t>& words);
} // namespace eve::gpgpu
+36 -1
View File
@@ -1,4 +1,5 @@
#include "gpgpu/Gpgpu.h"
#include "gpgpu/ComputeProgram.h"
#include "gpgpu/ComputeShader.h"
#include "gpgpu/EcsScriptPack.h"
#include "gpgpu/GpuBuffer.h"
@@ -204,6 +205,36 @@ bool Gpgpu::isAvailable() const {
#endif
}
Result<std::vector<uint32_t>> compileComputeSpirv(const std::string &source) {
#ifdef EVENGINE_WEBGPU
return Result<std::vector<uint32_t>>::failure(
Diagnostic::error(DiagnosticCode::Unsupported, "GLSL compilation requires Vulkan"));
#else
try {
return Result<std::vector<uint32_t>>::success(compileComputeGlsl(source));
} catch (const std::exception &e) {
return Result<std::vector<uint32_t>>::failure(Diagnostic::error(DiagnosticCode::Failed, e.what()));
}
#endif
}
Result<std::unique_ptr<ComputeShader>> createComputeShader(const std::vector<uint32_t> &words) {
#ifdef EVENGINE_WEBGPU
return Result<std::unique_ptr<ComputeShader>>::failure(
Diagnostic::error(DiagnosticCode::Unsupported, "SPIR-V pipelines require Vulkan"));
#else
try {
if (words.size() < 5 || words.front() != 0x07230203)
return Result<std::unique_ptr<ComputeShader>>::failure(
Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid SPIR-V header"));
return Result<std::unique_ptr<ComputeShader>>::success(
std::unique_ptr<ComputeShader>(vulkanNewShaderFromSpirv(words)));
} catch (const std::exception &e) {
return Result<std::unique_ptr<ComputeShader>>::failure(Diagnostic::error(DiagnosticCode::Failed, e.what()));
}
#endif
}
ComputeShader *Gpgpu::newShader(const std::string &source) {
#ifdef EVENGINE_WEBGPU
if (currentGraphicsBackend() != "webgpu")
@@ -213,7 +244,11 @@ ComputeShader *Gpgpu::newShader(const std::string &source) {
#else
if (currentGraphicsBackend() != "vulkan")
throw Exception("Gpgpu.newShader: requires vulkan Graphics backend");
return vulkanNewShaderFromSpirv(compileComputeGlsl(source));
auto compiled = compileComputeSpirv(source);
if (!compiled.ok()) throw Exception("%s", compiled.error()->message().c_str());
auto shader = createComputeShader(compiled.value());
if (!shader.ok()) throw Exception("%s", shader.error()->message().c_str());
return shader.value().release();
#endif
}
+5 -1
View File
@@ -26,7 +26,11 @@ public:
/** @brief True when the active Graphics backend can run compute (device initialized). */
bool isAvailable() const;
/** @brief Compile compute source for the active backend (Vulkan: GLSL; WebGPU: WGSL). */
/** @brief Compatibility-only raw-owning shader factory (Vulkan: GLSL; WebGPU: WGSL).
* Vulkan delegates to the
* checked compileComputeSpirv/createComputeShader APIs.
* Device thread only; caller must delete the shader
* before Graphics retires. */
ComputeShader *newShader(const std::string &source);
/** @brief Load precompiled compute bytecode from Filesystem path (Vulkan: SPIR-V). */
+2 -3
View File
@@ -14,7 +14,7 @@ ComputeShader *vulkanNewShaderFromSpirv(const std::vector<uint32_t> &spv) {
auto *vkg = requireVulkanGraphics();
auto &device = vkg->getDevice();
auto *shader = new VulkanComputeShader();
auto shader = std::make_unique<VulkanComputeShader>();
shader->device_ = &device;
shader->module_ = vkb::PipelineBuilder::createShaderModule(device.instance, spv);
@@ -56,11 +56,10 @@ ComputeShader *vulkanNewShaderFromSpirv(const std::vector<uint32_t> &spv) {
cpInfo.layout = shader->pipelineLayout_;
auto result = device->createComputePipeline(vk::PipelineCache{}, cpInfo, device.allocation_callbacks);
if (result.result != vk::Result::eSuccess) {
delete shader;
throw Exception("Gpgpu.newShader: createComputePipeline failed");
}
shader->pipeline_ = result.value;
return shader;
return shader.release();
}
GpuBuffer *vulkanNewBuffer(int byteSize, const std::string &usage) {
+13 -2
View File
@@ -75,6 +75,16 @@ void VulkanSequence::destroy() {
(*dev)->freeCommandBuffers(pool, submittedCommandBuffer);
submittedCommandBuffer = vk::CommandBuffer{};
}
// Abandoning an unsubmitted recording must retire its descriptor leases too.
// The caller keeps shaders alive until the Sequence has been destroyed.
for (ComputeShader *shader : usedShaders) {
auto *vs = dynamic_cast<VulkanComputeShader *>(shader);
if (vs) {
vs->endSequence();
vs->releasePendingDescriptors(*dev);
}
}
usedShaders.clear();
if (fenceReady) {
(*dev)->destroyFence(fence, dev->allocation_callbacks);
fence = nullptr;
@@ -117,15 +127,16 @@ void vulkanSequenceRecordUpload(VulkanSequence *seq, GpuBuffer *dst,
vkb::GenericBuffer *staging = nullptr;
for (size_t i = seq->stagingUsed; i < seq->stagingPool.size(); ++i) {
if (seq->stagingPool[i].capacity >= vk::DeviceSize(nbytes)) {
staging = &seq->stagingPool[i];
std::swap(seq->stagingPool[seq->stagingUsed], seq->stagingPool[i]);
staging = &seq->stagingPool[seq->stagingUsed];
break;
}
}
if (!staging) {
seq->stagingPool.emplace_back(device, buf::eTransferSrc, vk::DeviceSize(nbytes),
pfb::eHostVisible | pfb::eHostCoherent);
staging = &seq->stagingPool.back();
std::swap(seq->stagingPool[seq->stagingUsed], seq->stagingPool.back());
staging = &seq->stagingPool[seq->stagingUsed];
}
++seq->stagingUsed;
staging->updateLocal(vkb::FrameSlot::gpuIdle(), src, vk::DeviceSize(nbytes));
+131 -80
View File
@@ -10,6 +10,7 @@
#include <cstdlib>
#include <cstring>
#include <memory>
#include <stdexcept>
#include <vector>
#if defined(_WIN32)
@@ -24,12 +25,26 @@
namespace eve::gpgpu {
namespace {
// Compiler failures must not call Exception's global render-tracer callback:
// this path also executes on CPU workers without a Graphics lifetime.
std::runtime_error compileError(const char *message) {
return std::runtime_error(message);
}
template <class... Args>
std::runtime_error compileError(const char *format, Args... args) {
const int count = std::snprintf(nullptr, 0, format, args...);
if (count < 0) return std::runtime_error("Compute compilation failed");
std::string text(static_cast<size_t>(count) + 1, '\0');
std::snprintf(text.data(), text.size(), format, args...);
text.pop_back();
return std::runtime_error(text);
}
std::vector<uint32_t> loadSpirvBytes(const void *data, size_t size) {
if (!data || size < 4 || (size % 4) != 0)
throw Exception("Gpgpu SPIR-V: invalid size %zu", size);
if (!data || size < 4 || (size % 4) != 0) throw compileError("Gpgpu SPIR-V: invalid size %zu", size);
const auto *words = static_cast<const uint32_t *>(data);
if (words[0] != 0x07230203)
throw Exception("Gpgpu SPIR-V: bad magic (expected 0x07230203)");
if (words[0] != 0x07230203) throw compileError("Gpgpu SPIR-V: bad magic (expected 0x07230203)");
return std::vector<uint32_t>(words, words + size / 4);
}
@@ -85,40 +100,54 @@ namespace {
#if defined(EVE_HAS_SHADERC)
/** In-process GLSL -> SPIR-V via the Vulkan SDK's static shaderc library. */
std::vector<uint32_t> compileComputeGlslInProcess(const std::string &glsl) {
shaderc_compiler_t compiler = shaderc_compiler_initialize();
if (!compiler) throw Exception("Gpgpu.newShader: shaderc_compiler_initialize failed");
shaderc_compile_options_t options = shaderc_compile_options_initialize();
if (!options) {
shaderc_compiler_release(compiler);
throw Exception("Gpgpu.newShader: shaderc_compile_options_initialize failed");
}
shaderc_compile_options_set_target_env(options, shaderc_target_env_vulkan,
shaderc_env_version_vulkan_1_0);
shaderc_compile_options_set_target_spirv(options, shaderc_spirv_version_1_0);
struct CompilerContext {
shaderc_compiler_t compiler = shaderc_compiler_initialize();
shaderc_compile_options_t options = nullptr;
CompilerContext() {
if (!compiler) throw compileError("Gpgpu.newShader: shaderc_compiler_initialize failed");
options = shaderc_compile_options_initialize();
if (!options) {
shaderc_compiler_release(compiler);
throw compileError("Gpgpu.newShader: shaderc_compile_options_initialize failed");
}
shaderc_compile_options_set_target_env(options, shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_0);
shaderc_compile_options_set_target_spirv(options, shaderc_spirv_version_1_0);
}
~CompilerContext() {
shaderc_compile_options_release(options);
shaderc_compiler_release(compiler);
}
};
// Keep glslang's compiler/built-in state alive across jobs and graph segments.
// Each CPU thread owns its context; no device references survive here.
thread_local CompilerContext context;
auto compiler = context.compiler;
auto options = context.options;
shaderc_compilation_result_t result =
shaderc_compile_into_spv(compiler, glsl.data(), glsl.size(),
shaderc_glsl_compute_shader, "eve_compute.comp", "main",
options);
shaderc_compile_options_release(options);
shaderc_compiler_release(compiler);
if (!result) throw compileError("Gpgpu.newShader: shaderc returned no compilation result");
struct CompilationResult {
shaderc_compilation_result_t value;
~CompilationResult() { shaderc_result_release(value); }
} ownedResult{result};
if (shaderc_result_get_compilation_status(result) != shaderc_compilation_status_success) {
const std::string err = shaderc_result_get_error_message(result);
shaderc_result_release(result);
throw Exception("Gpgpu.newShader: shaderc failed:\n%s", err.c_str());
throw compileError("Gpgpu.newShader: shaderc failed:\n%s", err.c_str());
}
const size_t len = shaderc_result_get_length(result);
const char *bytes = shaderc_result_get_bytes(result);
if (len == 0 || len % 4 != 0) {
shaderc_result_release(result);
throw Exception("Gpgpu.newShader: shaderc returned invalid SPIR-V");
throw compileError("Gpgpu.newShader: shaderc returned invalid SPIR-V");
}
std::vector<uint32_t> spv(len / 4);
std::memcpy(spv.data(), bytes, len);
shaderc_result_release(result);
return spv;
}
#endif // EVE_HAS_SHADERC
#if !defined(EVE_HAS_SHADERC)
/** Locate a usable glslc.exe: VULKAN_SDK, common install roots, then PATH. */
std::string findGlslc() {
if (const char *sdk = std::getenv("VULKAN_SDK"); sdk && *sdk) {
@@ -157,37 +186,45 @@ std::string findGlslc() {
return {};
}
#endif // !EVE_HAS_SHADERC
} // namespace
#endif
std::vector<uint32_t> compileComputeGlsl(const std::string &glsl) {
if (glsl.empty()) throw Exception("Gpgpu.newShader: empty GLSL");
if (glsl.empty()) throw compileError("Gpgpu.newShader: empty GLSL");
#if defined(_WIN32)
#if defined(EVE_HAS_SHADERC)
try {
return compileComputeGlslInProcess(glsl);
} catch (...) {
// Fall through to spawning glslc (SDK lib present but compile failed).
}
#endif
// Compile errors remain errors; a packaged shaderc build never needs an SDK executable.
return compileComputeGlslInProcess(glsl);
#else
const std::string glslc = findGlslc();
if (glslc.empty())
throw Exception("Gpgpu.newShader: glslc not found on Windows "
"(install the Vulkan SDK or set VULKAN_SDK)");
throw compileError(
"Gpgpu.newShader: glslc not found on Windows "
"(install the Vulkan SDK or set VULKAN_SDK)");
char tmpDir[MAX_PATH];
if (GetTempPathA(MAX_PATH, tmpDir) == 0)
throw Exception("Gpgpu.newShader: GetTempPath failed");
if (GetTempPathA(MAX_PATH, tmpDir) == 0) throw compileError("Gpgpu.newShader: GetTempPath failed");
char inPath[MAX_PATH];
if (GetTempFileNameA(tmpDir, "eve", 0, inPath) == 0)
throw Exception("Gpgpu.newShader: GetTempFileName failed");
if (GetTempFileNameA(tmpDir, "eve", 0, inPath) == 0) throw compileError("Gpgpu.newShader: GetTempFileName failed");
std::string outPath = std::string(inPath) + ".spv";
const std::string errPath = outPath + ".err";
struct TemporaryFiles {
const char *input;
const std::string &output;
const std::string &error;
~TemporaryFiles() {
DeleteFileA(error.c_str());
DeleteFileA(output.c_str());
// Reserve the unique basename until both sidecars are consumed.
DeleteFileA(input);
}
} temporary{inPath, outPath, errPath};
{
FILE *f = nullptr;
if (fopen_s(&f, inPath, "wb") != 0 || !f) {
DeleteFileA(inPath);
throw Exception("Gpgpu.newShader: failed to write temp GLSL");
throw compileError("Gpgpu.newShader: failed to write temp GLSL");
}
fwrite(glsl.data(), 1, glsl.size(), f);
fclose(f);
@@ -196,64 +233,79 @@ std::vector<uint32_t> compileComputeGlsl(const std::string &glsl) {
// Spawn glslc directly with CreateProcess: _popen routes through cmd.exe
// /c whose quote-stripping rules mangle commands starting with a quoted
// program path. Capture stderr/stdout into a sidecar file.
const std::string errPath = outPath + ".err";
std::string cmd = "\"" + glslc + "\" -fshader-stage=comp \"" + inPath + "\" -o \"" +
outPath + "\"";
STARTUPINFOA si{};
si.cb = sizeof(si);
STARTUPINFOEXA si{};
si.StartupInfo.cb = sizeof(si);
PROCESS_INFORMATION pi{};
SECURITY_ATTRIBUTES sa{};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
HANDLE errFile = CreateFileA(errPath.c_str(), GENERIC_WRITE, FILE_SHARE_READ, &sa,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (errFile != INVALID_HANDLE_VALUE) {
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
si.hStdOutput = errFile;
si.hStdError = errFile;
HANDLE errFile = CreateFileA(errPath.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, &sa,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
HANDLE input = CreateFileA("NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (errFile == INVALID_HANDLE_VALUE || input == INVALID_HANDLE_VALUE) {
if (errFile != INVALID_HANDLE_VALUE) CloseHandle(errFile);
if (input != INVALID_HANDLE_VALUE) CloseHandle(input);
throw compileError("Gpgpu.newShader: failed to create compiler standard handles");
}
char *mutableCmd = _strdup(cmd.c_str());
struct StandardHandles {
HANDLE output, input;
~StandardHandles() {
CloseHandle(output);
CloseHandle(input);
}
} handles{errFile, input};
// Without an explicit list, concurrent CreateProcess calls inherit other
// jobs' temporary-file handles and can prevent glslc from opening its input.
SIZE_T attributeBytes = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &attributeBytes);
std::vector<uint8_t> attributes(attributeBytes);
si.lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributes.data());
if (!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeBytes))
throw compileError("Gpgpu.newShader: failed to initialize process attributes");
struct AttributeList {
LPPROC_THREAD_ATTRIBUTE_LIST value;
~AttributeList() { DeleteProcThreadAttributeList(value); }
} attributeList{si.lpAttributeList};
HANDLE inherited[] = {errFile, input};
if (!UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited,
sizeof(inherited), nullptr, nullptr))
throw compileError("Gpgpu.newShader: failed to restrict compiler handle inheritance");
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
si.StartupInfo.hStdInput = input;
si.StartupInfo.hStdOutput = errFile;
si.StartupInfo.hStdError = errFile;
const BOOL spawned =
CreateProcessA(glslc.c_str(), mutableCmd, nullptr, nullptr, TRUE, CREATE_NO_WINDOW,
nullptr, nullptr, &si, &pi);
free(mutableCmd);
if (errFile != INVALID_HANDLE_VALUE) CloseHandle(errFile);
CreateProcessA(glslc.c_str(), cmd.data(), nullptr, nullptr, TRUE,
CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &si.StartupInfo, &pi);
if (!spawned) {
const DWORD errCode = GetLastError();
DeleteFileA(inPath);
DeleteFileA(outPath.c_str());
DeleteFileA(errPath.c_str());
throw Exception("Gpgpu.newShader: failed to launch glslc (error %lu)", errCode);
throw compileError("Gpgpu.newShader: failed to launch glslc (error %lu)", errCode);
}
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(pi.hProcess, &exitCode);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
DeleteFileA(inPath);
if (exitCode != 0) {
std::string err;
FILE *ef = nullptr;
if (fopen_s(&ef, errPath.c_str(), "rb") == 0 && ef) {
char buf[512];
size_t n;
while ((n = fread(buf, 1, sizeof(buf) - 1, ef)) > 0) {
buf[n] = '\0';
err += buf;
}
fclose(ef);
HANDLE diagnostics =
CreateFileA(errPath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (diagnostics != INVALID_HANDLE_VALUE) {
char buffer[512];
DWORD count = 0;
while (ReadFile(diagnostics, buffer, sizeof(buffer), &count, nullptr) && count) err.append(buffer, count);
CloseHandle(diagnostics);
}
DeleteFileA(outPath.c_str());
DeleteFileA(errPath.c_str());
throw Exception("Gpgpu.newShader: glslc failed (exit %lu):\n%s", exitCode, err.c_str());
throw compileError("Gpgpu.newShader: glslc failed (exit %lu):\n%s", exitCode, err.c_str());
}
DeleteFileA(errPath.c_str());
FILE *f = nullptr;
if (fopen_s(&f, outPath.c_str(), "rb") != 0 || !f) {
DeleteFileA(outPath.c_str());
throw Exception("Gpgpu.newShader: failed to open compiled SPIR-V");
throw compileError("Gpgpu.newShader: failed to open compiled SPIR-V");
}
fseek(f, 0, SEEK_END);
long sz = ftell(f);
@@ -261,23 +313,22 @@ std::vector<uint32_t> compileComputeGlsl(const std::string &glsl) {
std::vector<uint8_t> bytes(static_cast<size_t>(sz > 0 ? sz : 0));
if (sz > 0 && fread(bytes.data(), 1, static_cast<size_t>(sz), f) != static_cast<size_t>(sz)) {
fclose(f);
DeleteFileA(outPath.c_str());
throw Exception("Gpgpu.newShader: failed to read compiled SPIR-V");
throw compileError("Gpgpu.newShader: failed to read compiled SPIR-V");
}
fclose(f);
DeleteFileA(outPath.c_str());
return loadSpirvBytes(bytes.data(), bytes.size());
#endif // EVE_HAS_SHADERC
#else
char inPath[] = "/tmp/eve_comp_XXXXXX";
int fd = mkstemp(inPath);
if (fd < 0) throw Exception("Gpgpu.newShader: mkstemp failed");
if (fd < 0) throw compileError("Gpgpu.newShader: mkstemp failed");
std::string outPath = std::string(inPath) + ".spv";
{
ssize_t n = write(fd, glsl.data(), glsl.size());
close(fd);
if (n < 0 || size_t(n) != glsl.size()) {
unlink(inPath);
throw Exception("Gpgpu.newShader: failed to write temp GLSL");
throw compileError("Gpgpu.newShader: failed to write temp GLSL");
}
}
@@ -292,17 +343,17 @@ std::vector<uint32_t> compileComputeGlsl(const std::string &glsl) {
unlink(inPath);
if (status != 0) {
unlink(outPath.c_str());
throw Exception("Gpgpu.newShader: glslc failed:\n%s", err.c_str());
throw compileError("Gpgpu.newShader: glslc failed:\n%s", err.c_str());
}
} else {
unlink(inPath);
throw Exception("Gpgpu.newShader: glslc not available (popen failed)");
throw compileError("Gpgpu.newShader: glslc not available (popen failed)");
}
FILE *f = fopen(outPath.c_str(), "rb");
if (!f) {
unlink(outPath.c_str());
throw Exception("Gpgpu.newShader: failed to open compiled SPIR-V");
throw compileError("Gpgpu.newShader: failed to open compiled SPIR-V");
}
fseek(f, 0, SEEK_END);
long sz = ftell(f);
@@ -311,7 +362,7 @@ std::vector<uint32_t> compileComputeGlsl(const std::string &glsl) {
if (sz > 0 && fread(bytes.data(), 1, static_cast<size_t>(sz), f) != static_cast<size_t>(sz)) {
fclose(f);
unlink(outPath.c_str());
throw Exception("Gpgpu.newShader: failed to read compiled SPIR-V");
throw compileError("Gpgpu.newShader: failed to read compiled SPIR-V");
}
fclose(f);
unlink(outPath.c_str());
+11
View File
@@ -31,6 +31,10 @@
struct aiMesh;
namespace eve {
class Subscription;
}
namespace eve::graphics {
struct PbrSurface;
@@ -188,6 +192,13 @@ public:
* callbacks.
*/
[[nodiscard]] std::weak_ptr<const void> resourceLifetime() const;
/** @brief Register cleanup before this provider destroys its GPU device/resources.
* @return Owning subscription, or Conflict if retirement has started.
* @lifetime Retain the subscription until cleanup is no longer needed. Either destruction order is safe.
* @thread Render thread only. Callback must not throw, recreate or destroy Graphics, or start GPU work.
* @note Callbacks run without locks; disposing subscriptions during notification is supported. */
[[nodiscard]] Result<eve::Subscription> onResourcesRetiring(std::function<void()> callback) const;
/**
* @brief Whether gbuffer-based post-process shaders (AO, GI) can be created on this
+29 -8
View File
@@ -1,13 +1,16 @@
#include <cstdio>
#include <unordered_map>
#include "common/Subscription.h"
#include "graphics/Graphics.h"
namespace eve::graphics {
namespace {
// Render-thread-only observation registry. Erasing on destruction prevents address reuse
// from making an old observer valid for a new Graphics instance.
struct Lifetime {
eve::Observer<> retiring;
bool retired = false;
};
bool registryAlive = false;
struct Registry {
std::unordered_map<const Graphics*, std::shared_ptr<const void>> tokens;
std::unordered_map<const Graphics*, std::shared_ptr<Lifetime>> tokens;
Registry() { registryAlive = true; }
~Registry() { registryAlive = false; }
};
@@ -15,13 +18,31 @@ auto& lifetimes() {
static Registry registry;
return registry.tokens;
}
auto lifetime(const Graphics* graphics) {
auto& value = lifetimes()[graphics];
if (!value) value = std::make_shared<Lifetime>();
return value;
}
} // namespace
std::weak_ptr<const void> Graphics::resourceLifetime() const {
auto& token = lifetimes()[this];
if (!token) token = std::make_shared<const int>(0);
return token;
auto value = lifetime(this);
return value->retired ? std::weak_ptr<const void>{} : value;
}
Result<eve::Subscription> Graphics::onResourcesRetiring(std::function<void()> callback) const {
auto value = lifetime(this);
if (value->retired)
return Result<eve::Subscription>::failure(
Diagnostic::error(DiagnosticCode::Conflict, "Graphics resources are retiring"));
return Result<eve::Subscription>::success(value->retiring.subscribe(std::move(callback)));
}
void Graphics::retireResourceLifetime() const {
if (registryAlive) lifetimes().erase(this);
if (!registryAlive) return;
auto it = lifetimes().find(this);
if (it == lifetimes().end() || it->second->retired) return;
auto value = it->second;
value->retired = true;
const auto failures = value->retiring.notifyChecked([]() noexcept {});
if (failures) std::fprintf(stderr, "Graphics resource retirement: %zu callback failures\n", failures);
lifetimes().erase(this);
}
} // namespace eve::graphics
+1
View File
@@ -292,6 +292,7 @@ std::string Graphics::getBackendName() const { return "vulkan"; }
Graphics::Graphics() = default;
Graphics::~Graphics() {
retireResourceLifetime();
detachGraphicsArtifactProvider(this);
if (!initialized) {
// A process-level boot warmup may exist even when callers decide not
+4 -1
View File
@@ -99,7 +99,10 @@ Graphics::Graphics() {
}
}
Graphics::~Graphics() { detachGraphicsArtifactProvider(this); }
Graphics::~Graphics() {
retireResourceLifetime();
detachGraphicsArtifactProvider(this);
}
// ---------------------------------------------------------------------------
// Init
+179
View File
@@ -0,0 +1,179 @@
#include "tensor/AffineQuant.h"
#include "tensor/AffineQuantInternal.h"
#include "tensor/OnnxGpuKernels.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace eve::tensor::affine {
namespace {
template <class T>
Result<T> fail(const char* message) {
return Result<T>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, message));
}
int value(ByteView v, size_t i) {
const int x = v.bytes[i];
return v.signedValues && x >= 128 ? x - 256 : x;
}
bool validZero(int z, bool sign) { return sign ? z >= -128 && z <= 127 : z >= 0 && z <= 255; }
double evenRound(double x) {
const double lo = std::floor(x), fraction = x - lo;
return lo + (fraction > 0.5 || (fraction == 0.5 && std::fmod(lo, 2.0) != 0));
}
bool fits(int64_t n) { return n >= INT32_MIN && n <= INT32_MAX; }
bool product(size_t a, size_t b, size_t limit) { return b == 0 || a <= limit / b; }
} // namespace
Result<std::vector<uint8_t>> quantize(std::span<const float> input, float scale, int zero, bool sign) {
if (!(scale > 0) || !std::isfinite(scale) || !validZero(zero, sign))
return fail<std::vector<uint8_t>>("Invalid affine scale or zero point");
std::vector<uint8_t> out;
out.reserve(input.size());
for (float x : input) {
if (!std::isfinite(x)) return fail<std::vector<uint8_t>>("Nonfinite quantization input");
const double rounded = evenRound(static_cast<double>(x) / scale) + zero;
const int q = static_cast<int>(std::clamp(rounded, sign ? -128.0 : 0.0, sign ? 127.0 : 255.0));
out.push_back(static_cast<uint8_t>(q));
}
return Result<std::vector<uint8_t>>::success(std::move(out));
}
Result<QuantizedActivation> dynamicQuantize(std::span<const float> input) {
float lo = 0, hi = 0;
for (float x : input) {
if (!std::isfinite(x)) return fail<QuantizedActivation>("Nonfinite dynamic quantization input");
lo = std::min(lo, x);
hi = std::max(hi, x);
}
QuantizedActivation result;
result.scale = hi == lo ? 1.f : static_cast<float>((static_cast<double>(hi) - lo) / 255.0);
if (!(result.scale > 0) || !std::isfinite(result.scale))
return fail<QuantizedActivation>("Dynamic quantization scale is not representable");
result.zeroPoint = static_cast<uint8_t>(std::clamp(evenRound(-static_cast<double>(lo) / result.scale), 0.0, 255.0));
auto bytes = quantize(input, result.scale, result.zeroPoint, false);
if (!bytes.ok()) return Result<QuantizedActivation>::failure(bytes.status());
result.values = std::move(bytes.value());
return Result<QuantizedActivation>::success(std::move(result));
}
Result<std::vector<float>> dequantize(ByteView input, std::span<const float> scales, std::span<const int32_t> zeros,
size_t inner) {
if (scales.empty() || scales.size() != zeros.size() || inner == 0 || !product(scales.size(), inner, SIZE_MAX) ||
input.bytes.size() % (scales.size() * inner))
return fail<std::vector<float>>("Invalid affine channel layout");
for (size_t i = 0; i < scales.size(); ++i)
if (!(scales[i] > 0) || !std::isfinite(scales[i]) || !validZero(zeros[i], input.signedValues))
return fail<std::vector<float>>("Invalid affine scale or zero point");
std::vector<float> out(input.bytes.size());
for (size_t i = 0; i < out.size(); ++i) {
const size_t channel = (i / inner) % scales.size();
out[i] = static_cast<float>(value(input, i) - zeros[channel]) * scales[channel];
}
return Result<std::vector<float>>::success(std::move(out));
}
Result<std::vector<int32_t>> matmul(ByteView a, ByteView b, size_t m, size_t k, size_t n, int aZero, int bZero,
OnnxCompute* compute) {
if (!validZero(aZero, a.signedValues) || !validZero(bZero, b.signedValues) || !product(m, k, SIZE_MAX) ||
!product(k, n, SIZE_MAX) || !product(m, n, INT32_MAX) || a.bytes.size() != m * k || b.bytes.size() != k * n)
return fail<std::vector<int32_t>>("Invalid integer matmul dimensions or zero points");
if (compute && m && n && k) {
try {
const int32_t z = bZero;
return Result<std::vector<int32_t>>::success(
onnx_detail::gpuMatmul(*compute, a, b, m, k, n, aZero, {&z, 1}));
} catch (const std::exception& e) {
return Result<std::vector<int32_t>>::failure(Diagnostic::error(DiagnosticCode::Failed, e.what()));
}
}
std::vector<int32_t> out(m * n);
for (size_t r = 0; r < m; ++r)
for (size_t c = 0; c < n; ++c) {
int64_t sum = 0;
for (size_t j = 0; j < k; ++j)
sum += static_cast<int64_t>(value(a, r * k + j) - aZero) * (value(b, j * n + c) - bZero);
if (!fits(sum)) return fail<std::vector<int32_t>>("Integer matmul accumulator overflow");
out[r * n + c] = static_cast<int32_t>(sum);
}
return Result<std::vector<int32_t>>::success(std::move(out));
}
Result<detail::ConvExtent> detail::validateConv(size_t xSize, size_t wSize, bool xSigned, bool wSigned,
const ConvShape& s, int xZero, std::span<const int32_t> wZeros) {
for (int d : {s.batch, s.channels, s.height, s.width, s.outputs, s.kernelH, s.kernelW, s.strideH, s.strideW,
s.dilationH, s.dilationW, s.groups})
if (d <= 0 || d > 65536) return fail<detail::ConvExtent>("Invalid integer convolution dimensions");
if (s.channels % s.groups || s.outputs % s.groups || s.padTop < 0 || s.padBottom < 0 || s.padLeft < 0 ||
s.padRight < 0 || !validZero(xZero, xSigned) ||
(wZeros.size() != 1 && wZeros.size() != static_cast<size_t>(s.outputs)))
return fail<detail::ConvExtent>("Invalid convolution groups, padding or zero points");
for (int z : wZeros)
if (!validZero(z, wSigned)) return fail<detail::ConvExtent>("Invalid convolution weight zero point");
const int64_t h = static_cast<int64_t>(s.height) + s.padTop + s.padBottom -
static_cast<int64_t>(s.dilationH) * (s.kernelH - 1) - 1;
const int64_t width = static_cast<int64_t>(s.width) + s.padLeft + s.padRight -
static_cast<int64_t>(s.dilationW) * (s.kernelW - 1) - 1;
if (h < 0 || width < 0) return fail<detail::ConvExtent>("Convolution kernel exceeds padded input");
const int64_t oh = h / s.strideH + 1, ow = width / s.strideW + 1;
int64_t outCount = 1;
for (int64_t d : {static_cast<int64_t>(s.batch), static_cast<int64_t>(s.outputs), oh, ow}) {
if (d > INT32_MAX / outCount) return fail<detail::ConvExtent>("Convolution output too large");
outCount *= d;
}
const int cg = s.channels / s.groups;
size_t inputCount = 1, weightCount = 1;
for (int d : {s.batch, s.channels, s.height, s.width}) {
if (!product(inputCount, d, INT32_MAX)) return fail<detail::ConvExtent>("Convolution input too large");
inputCount *= d;
}
for (int d : {s.outputs, cg, s.kernelH, s.kernelW}) {
if (!product(weightCount, d, INT32_MAX)) return fail<detail::ConvExtent>("Convolution weights too large");
weightCount *= d;
}
if (outCount > INT32_MAX || xSize != inputCount || wSize != weightCount)
return fail<detail::ConvExtent>("Convolution buffer size mismatch or output too large");
return Result<detail::ConvExtent>::success({oh, ow, outCount});
}
Result<std::vector<int32_t>> conv(ByteView x, ByteView w, const ConvShape& s, int xZero,
std::span<const int32_t> wZeros, OnnxCompute* compute) {
auto extent =
detail::validateConv(x.bytes.size(), w.bytes.size(), x.signedValues, w.signedValues, s, xZero, wZeros);
if (!extent.ok()) return Result<std::vector<int32_t>>::failure(extent.status());
const auto [oh, ow, outCount] = extent.value();
const int cg = s.channels / s.groups;
if (compute) {
try {
return Result<std::vector<int32_t>>::success(onnx_detail::gpuConv(*compute, x, w, s, xZero, wZeros));
} catch (const std::exception& e) {
return Result<std::vector<int32_t>>::failure(Diagnostic::error(DiagnosticCode::Failed, e.what()));
}
}
std::vector<int32_t> out(static_cast<size_t>(outCount));
for (int b = 0; b < s.batch; ++b)
for (int o = 0; o < s.outputs; ++o)
for (int64_t y = 0; y < oh; ++y)
for (int64_t z = 0; z < ow; ++z) {
int64_t sum = 0;
for (int c = 0; c < cg; ++c)
for (int ky = 0; ky < s.kernelH; ++ky)
for (int kx = 0; kx < s.kernelW; ++kx) {
const int64_t iy = y * s.strideH - s.padTop + static_cast<int64_t>(ky) * s.dilationH;
const int64_t ix = z * s.strideW - s.padLeft + static_cast<int64_t>(kx) * s.dilationW;
if (iy < 0 || ix < 0 || iy >= s.height || ix >= s.width) continue;
const size_t xi =
((static_cast<size_t>(b) * s.channels + (o / (s.outputs / s.groups)) * cg + c) *
s.height +
iy) *
s.width +
ix;
const size_t wi = ((static_cast<size_t>(o) * cg + c) * s.kernelH + ky) * s.kernelW + kx;
sum += static_cast<int64_t>(value(x, xi) - xZero) *
(value(w, wi) - wZeros[wZeros.size() == 1 ? 0 : o]);
}
if (!fits(sum)) return fail<std::vector<int32_t>>("Integer convolution accumulator overflow");
out[((static_cast<size_t>(b) * s.outputs + o) * oh + y) * ow + z] = static_cast<int32_t>(sum);
}
return Result<std::vector<int32_t>>::success(std::move(out));
}
} // namespace eve::tensor::affine
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include "common/Result.h"
#include <cstdint>
#include <span>
#include <vector>
namespace eve::tensor {
class OnnxCompute;
}
namespace eve::tensor::affine {
/** @brief ONNX unsigned activation quantization; owns bytes and scalar parameters. */
struct QuantizedActivation {
std::vector<uint8_t> values;
float scale = 1;
uint8_t zeroPoint = 0;
};
/** @brief Borrowed packed 8-bit values, valid for the duration of a synchronous call. */
struct ByteView {
std::span<const uint8_t> bytes;
bool signedValues = false;
};
/**
* @brief Quantize finite FP32 activations with ONNX DynamicQuantizeLinear semantics.
* @param input Borrowed flat input; not retained.
* @return Owning uint8 values, scale, zero point; InvalidArgument for nonfinite input.
* @note CPU, thread-safe, no callbacks; nearest-even rounding independent of fenv.
*/
[[nodiscard]] Result<QuantizedActivation> dynamicQuantize(std::span<const float> input);
/**
* @brief Affine quantization to int8/uint8 bytes, saturating and rounding ties to even.
* @return Owning packed bytes; rejects invalid scale, zero point or nonfinite input.
* @note CPU, thread-safe; spans are borrowed only during the call.
*/
[[nodiscard]] Result<std::vector<uint8_t>> quantize(std::span<const float> input, float scale, int zeroPoint,
bool signedValues);
/**
* @brief Affine dequantization using scalar or per-axis scale/zero point.
* @param inner Number of contiguous values following the channel axis.
* @return Owning FP32 values; InvalidArgument on inconsistent channel layout.
* @note CPU, thread-safe; all views are borrowed for the call, no callbacks.
*/
[[nodiscard]] Result<std::vector<float>> dequantize(ByteView input, std::span<const float> scales,
std::span<const int32_t> zeros, size_t inner = 1);
/**
* @brief Integer row-major [M,K] x [K,N], subtracting scalar zero points.
* @return Exact owning int32 accumulators; rejects dimension and int32 overflow.
* @param compute Optional borrowed GPU provider; null selects CPU. No retry on GPU failure.
* @note Synchronous. CPU is thread-safe; GPU uses the provider thread. Weights remain packed.
*/
[[nodiscard]] Result<std::vector<int32_t>> matmul(ByteView a, ByteView b, size_t m, size_t k, size_t n, int aZero = 0,
int bZero = 0, OnnxCompute* compute = nullptr);
/** @brief Explicit 1D/2D NCHW convolution geometry; missing 1D height is one. */
struct ConvShape {
int batch = 1, channels = 1, height = 1, width = 1;
int outputs = 1, kernelH = 1, kernelW = 1;
int strideH = 1, strideW = 1, dilationH = 1, dilationW = 1;
int padTop = 0, padLeft = 0, padBottom = 0, padRight = 0, groups = 1;
};
/**
* @brief Integer Conv, supporting groups, asymmetric padding and dilation.
* @return Owning NCHW int32 values; rejects bad geometry or accumulator overflow.
* @param compute Optional borrowed GPU provider; null selects CPU. No retry on GPU failure.
* @note Synchronous. CPU is thread-safe; GPU uses the provider thread. Padding represents real zero.
*/
[[nodiscard]] Result<std::vector<int32_t>> conv(ByteView x, ByteView w, const ConvShape& shape, int xZero,
std::span<const int32_t> wZeros, OnnxCompute* compute = nullptr);
} // namespace eve::tensor::affine
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "tensor/AffineQuant.h"
namespace eve::tensor::affine::detail {
struct ConvExtent {
int64_t height, width, count;
};
Result<ConvExtent> validateConv(size_t xSize, size_t wSize, bool xSigned, bool wSigned, const ConvShape&, int,
std::span<const int32_t>);
} // namespace eve::tensor::affine::detail
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include <span>
#include <stdexcept>
#include "tensor/OnnxModel.h"
#include "tensor/OnnxStorage.h"
namespace eve::tensor::onnx_detail {
// Internal copy-on-write host view. Const reads materialize only at CPU boundaries.
// GPU aliases share immutable storage and never force a readback.
class ByteStorage {
mutable OnnxBuffer buffer_;
std::shared_ptr<std::vector<uint8_t>> writable_;
const std::vector<uint8_t>& get() const {
if (!buffer_.host) {
if (!buffer_.device) throw std::runtime_error("Missing ONNX storage");
auto r = buffer_.device->readback();
if (!r.ok()) throw std::runtime_error(r.error()->message());
if (r.value().size() != buffer_.size) throw std::runtime_error("GPU byte count mismatch");
buffer_.host = std::make_shared<const std::vector<uint8_t>>(std::move(r.value()));
}
return *buffer_.host;
}
std::vector<uint8_t>& write() {
// CPU mutation creates a new authoritative storage identity.
if (writable_ && buffer_.host.use_count() == 2) return *writable_;
auto v = std::make_shared<std::vector<uint8_t>>(get());
buffer_ = {v, {}, v->size()};
writable_ = v;
return *v;
}
public:
ByteStorage() : ByteStorage(std::vector<uint8_t>{}) {}
ByteStorage(std::vector<uint8_t> v) : buffer_{std::make_shared<const std::vector<uint8_t>>(std::move(v)), {}, 0} {
buffer_.size = buffer_.host->size();
}
ByteStorage(OnnxBuffer b) : buffer_(std::move(b)) {}
void retainAcrossRuns() { buffer_.persistent = true; }
const OnnxBuffer& buffer() const { return buffer_; }
size_t size() const { return buffer_.size; }
bool empty() const { return !size(); }
/** @brief Borrow CPU data until this storage is mutated or destroyed; device-thread only for lazy data.
* @lifetime Borrowed until mutation or destruction; use the provider thread for GPU readback.
* @throws std::runtime_error On readback failure. Internal executor converts this to Result. */
const uint8_t* data() const { return get().data(); }
/** @brief Borrow mutable CPU data until mutation/destruction; detaches graph aliases before writing.
* @lifetime Borrowed until mutation or destruction; use the provider thread for GPU readback.
* @throws std::runtime_error On readback failure. Internal executor converts this to Result. */
uint8_t* data() { return write().data(); }
uint8_t operator[](size_t i) const { return get()[i]; }
uint8_t& operator[](size_t i) { return write()[i]; }
auto begin() { return write().begin(); }
auto end() { return write().end(); }
auto begin() const { return get().begin(); }
auto end() const { return get().end(); }
void resize(size_t n) {
auto& v = write();
v.resize(n);
buffer_.size = n;
}
/** @brief Copy borrowed iterators into owning CPU storage.
* @lifetime Input iterators are borrowed only for this call; all prior data pointers are invalidated. */
template <class I>
void assign(I a, I b) {
*this = ByteStorage(std::vector<uint8_t>(a, b));
}
operator std::span<const uint8_t>() const { return get(); }
operator std::vector<uint8_t>() const { return get(); }
};
struct RuntimeTensor {
OnnxElement element = OnnxElement::Float32;
std::vector<int64_t> shape;
ByteStorage bytes;
};
} // namespace eve::tensor::onnx_detail
+99
View File
@@ -0,0 +1,99 @@
#pragma once
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <future>
#include <mutex>
#include <thread>
#include "gpgpu/ComputeProgram.h"
namespace eve::tensor::onnx_detail {
// Session-owned CPU workers. Jobs own their source and never capture Graphics,
// Run, buffers or callbacks. Joining the workers needs no device access.
struct CompiledProgram {
std::vector<uint32_t> words;
double milliseconds = 0;
};
class CompilerQueue {
using Job = std::packaged_task<CompiledProgram()>;
std::mutex mutex;
std::condition_variable ready;
std::deque<Job> jobs;
bool stopping = false;
size_t inFlight = 0;
std::vector<std::thread> workers;
std::atomic<size_t> running{0}, peak{0};
void stop() noexcept {
{
std::lock_guard lock(mutex);
stopping = true;
}
ready.notify_all();
for (auto& worker : workers)
if (worker.joinable()) worker.join();
}
public:
explicit CompilerQueue(uint32_t count) {
try {
for (uint32_t i = 0; i < count; ++i)
workers.emplace_back([this] {
for (;;) {
Job job;
{
std::unique_lock lock(mutex);
ready.wait(lock, [this] { return stopping || !jobs.empty(); });
if (jobs.empty()) return;
job = std::move(jobs.front());
jobs.pop_front();
++inFlight;
}
const auto n = ++running;
auto p = peak.load();
while (p < n && !peak.compare_exchange_weak(p, n)) {
}
job(); // packaged_task transports failures to the owning device thread.
--running;
{
std::lock_guard lock(mutex);
--inFlight;
}
ready.notify_all();
}
});
} catch (...) {
stop();
throw;
}
}
~CompilerQueue() { stop(); }
// Called on the device thread. Run bounds each segment to 48 operations;
// this independent queue bound also protects accidental future callers.
std::future<CompiledProgram> enqueue(std::string source) {
Job job([source = std::move(source)] {
const auto start = std::chrono::steady_clock::now();
auto result = gpgpu::compileComputeSpirv(source);
if (!result.ok()) throw std::runtime_error(result.error()->message() + "\nCompute source:\n" + source);
return CompiledProgram{
std::move(result.value()),
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count()};
});
auto future = job.get_future();
{
std::lock_guard lock(mutex);
if (stopping || jobs.size() >= 48) throw std::runtime_error("ONNX compiler queue unavailable");
jobs.push_back(std::move(job));
}
ready.notify_one();
return future;
}
size_t peakWorkers() const noexcept { return peak.load(); }
void waitIdle() noexcept {
std::unique_lock lock(mutex);
ready.wait(lock, [this] { return jobs.empty() && inFlight == 0; });
}
void resetPeak() noexcept { peak = 0; }
};
} // namespace eve::tensor::onnx_detail
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include <cstdint>
#include <memory>
#include <span>
#include <string>
#include <vector>
#include "common/Result.h"
#include "tensor/OnnxStorage.h"
namespace eve::tensor {
/** @brief Synchronous GPU kernel request; all inputs are borrowed only until dispatch returns. */
struct OnnxKernel {
std::string source;
std::vector<std::span<const uint8_t>> inputs;
size_t outputBytes = 0;
uint32_t workItems = 0;
};
/**
* @brief GPU execution boundary for native ONNX; retains no model and retains compiled resources for the lifetime of
* the provider.
* @note Use on the device's owning thread, without concurrent calls or reentrancy.
* dispatch completes GPU work before returning host bytes; enqueue may defer completion. Errors never
* trigger CPU execution. Integer kernels require exact int32 results.
*/
class OnnxCompute {
public:
/** @brief Release device resources after all synchronous calls have returned. */
virtual ~OnnxCompute() = default;
/** @brief Compatibility-only synchronous host-byte entry point; engine dispatch delegates to enqueue.
* @note GLSL 450, local_size_x=64; inputs occupy bindings 0..N-1, output N. */
[[nodiscard]] virtual Result<std::vector<uint8_t>> dispatch(const OnnxKernel& kernel) = 0;
/** @brief Queue a kernel using immutable host/device buffers; no readback is required.
* @note Default provider executes synchronously through dispatch. Engine provider records a
* resident graph segment inside runGpu, completing it at a host-read or resource-budget boundary.
* Inputs are retained until completion; returned device storage expires when runGpu ends. */
[[nodiscard]] virtual Result<OnnxBuffer> enqueue(const std::string& source, std::span<const OnnxBuffer> inputs,
size_t outputBytes, uint32_t workItems) {
OnnxKernel k{source, {}, outputBytes, workItems};
std::vector<std::vector<uint8_t>> copies;
copies.reserve(inputs.size());
for (const auto& input : inputs) {
if (input.host)
k.inputs.push_back(*input.host);
else {
if (!input.device)
return Result<OnnxBuffer>::failure(
Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing GPU input"));
auto r = input.device->readback();
if (!r.ok()) return Result<OnnxBuffer>::failure(r.status());
copies.push_back(std::move(r.value()));
k.inputs.push_back(copies.back());
}
}
auto r = dispatch(k);
if (!r.ok()) return Result<OnnxBuffer>::failure(r.status());
auto bytes = std::make_shared<const std::vector<uint8_t>>(std::move(r.value()));
return Result<OnnxBuffer>::success({bytes, {}, bytes->size()});
}
/** @brief Observable transfer counters for the current or most recent run. */
virtual OnnxTransferStats transferStats() const { return {}; }
protected:
friend class OnnxModel;
/** @brief Start a synchronous run cache scope; invoked only by OnnxModel on the device thread. */
virtual void beginRun() noexcept {}
/** @brief Retire temporary activations/recordings; compiled resources may survive for later calls. */
virtual void endRun() noexcept {}
};
/**
* @brief Create a reusable GPU session for the active engine Gpgpu Vulkan device, or an error.
* @note Retain and reuse this owning provider across runGpu calls. Pipelines, immutable initializers
* and buffer pools persist until provider destruction or Graphics resource retirement. Cache bounds:
* 1024 shader variants, 128 MiB initializer payload and 512 MiB device buffers.
* CPU shader compilation uses a
* bounded queue; pipeline creation and submission remain on the device thread.
* @param compilerWorkers Number of CPU
* compiler workers (1..8); default 4. Source is owned by each job.
* @lifetime Graphics retirement clears device
* resources before device destruction; either destruction order is supported. A retired session rejects further GPU
* work; create a new session for a new device.
* @thread Graphics thread only, no concurrent calls, reentrancy or device destruction during runGpu.
*/
[[nodiscard]] Result<std::unique_ptr<OnnxCompute>> createOnnxGpuCompute(uint32_t compilerWorkers = 4);
} // namespace eve::tensor
+377
View File
@@ -0,0 +1,377 @@
#include "tensor/CpuKernels.h"
#include "tensor/OnnxInternal.h"
#include "tensor/Tensor.h"
#include <algorithm>
#include <cmath>
#include <numeric>
#include <set>
namespace eve::tensor::onnx_detail {
bool isSupported(const Node& n) {
if (n.domain == "com.microsoft" && n.op == "DynamicQuantizeLSTM") {
if (n.inputs.size() != 12 || n.outputs.empty() || n.outputs.size() > 3) return false;
for (const auto& [key, a] : n.attrs) {
if (key == "direction") {
if (a.type != 3 || (a.text != "forward" && a.text != "reverse" && a.text != "bidirectional"))
return false;
} else if (key == "hidden_size" || key == "input_forget") {
if (a.type != 2) return false;
} else if (key == "clip") {
if (a.type != 1) return false;
} else
return false;
}
return true;
}
if (!n.domain.empty()) return false;
if (n.op == "If" || n.op == "Loop") {
const std::set<std::string> keys =
n.op == "If" ? std::set<std::string>{"then_branch", "else_branch"} : std::set<std::string>{"body"};
if (n.attrs.size() != keys.size() || n.outputs.empty() ||
(n.op == "If" ? n.inputs.size() != 1 : n.inputs.size() < 2))
return false;
for (const auto& key : keys) {
auto it = n.attrs.find(key);
if (it == n.attrs.end() || it->second.type != 5 || !it->second.graph) return false;
}
return true;
}
static const std::map<std::string, std::set<std::string>> allowed{
{"DynamicQuantizeLinear", {}},
{"QuantizeLinear", {"axis"}},
{"DequantizeLinear", {"axis"}},
{"MatMulInteger", {}},
{"ConvInteger", {"auto_pad", "dilations", "group", "kernel_shape", "pads", "strides"}},
{"Constant", {"value"}},
{"Clip", {}},
{"Pad", {"mode"}},
{"CumSum", {"exclusive", "reverse"}},
{"SequenceEmpty", {"dtype"}},
{"SequenceAt", {}},
{"SequenceInsert", {}},
{"SplitToSequence", {"axis", "keepdims"}},
{"ConcatFromSequence", {"axis", "new_axis"}},
{"RandomUniformLike", {"dtype", "seed", "low", "high"}},
{"RandomNormalLike", {"dtype", "seed", "mean", "scale"}},
{"InstanceNormalization", {"epsilon"}},
{"ConvTranspose", {"auto_pad", "dilations", "group", "kernel_shape", "output_padding", "pads", "strides"}},
{"Resize", {"coordinate_transformation_mode", "mode", "nearest_mode", "cubic_coeff_a"}},
{"Range", {}},
{"TopK", {"axis", "largest", "sorted"}},
{"ScatterND", {"reduction"}},
{"ScatterElements", {"axis", "reduction"}},
{"ReduceProd", {"axes", "keepdims"}},
{"ReduceMax", {"axes", "keepdims"}},
{"Slice", {}},
{"Concat", {"axis"}},
{"Expand", {}},
{"ConstantOfShape", {"value"}},
{"Where", {}},
{"Equal", {}},
{"Less", {}},
{"Greater", {}},
{"And", {}},
{"Not", {}},
{"Reciprocal", {}},
{"LeakyRelu", {"alpha"}},
{"Floor", {}},
{"Round", {}},
{"Atan", {}},
{"Identity", {}},
{"Cast", {"to"}},
{"Gather", {"axis"}},
{"Shape", {"start", "end"}},
{"Reshape", {"allowzero"}},
{"Transpose", {"perm"}},
{"Unsqueeze", {}},
{"Squeeze", {}},
{"MatMul", {}},
{"Add", {}},
{"Sub", {}},
{"Mul", {}},
{"Div", {}},
{"Sqrt", {}},
{"Exp", {}},
{"Log", {}},
{"Sin", {}},
{"Cos", {}},
{"Relu", {}},
{"Sigmoid", {}},
{"Tanh", {}},
{"Neg", {}},
{"Abs", {}},
{"Pow", {}},
{"Softmax", {"axis"}},
{"ReduceMean", {"axes", "keepdims"}},
{"ReduceSum", {"keepdims", "noop_with_empty_axes"}}};
auto op = allowed.find(n.op);
if (op == allowed.end()) return false;
size_t minimum = 1, maximum = 1, outputs = 1;
if (n.op == "Clip") maximum = 3;
if (n.op == "Pad") {
minimum = 2;
maximum = 3;
}
if (n.op == "CumSum") minimum = maximum = 2;
if (n.op == "SequenceEmpty") minimum = maximum = 0;
if (n.op == "SequenceAt") minimum = maximum = 2;
if (n.op == "SequenceInsert") {
minimum = 2;
maximum = 3;
}
if (n.op == "SplitToSequence") maximum = 2;
if (n.op == "InstanceNormalization") minimum = maximum = 3;
if (n.op == "ConvTranspose") {
minimum = 2;
maximum = 3;
}
if (n.op == "Resize") {
minimum = 3;
maximum = 4;
}
if (n.op == "Range" || n.op == "ScatterElements" || n.op == "ScatterND") minimum = maximum = 3;
if (n.op == "TopK") {
minimum = maximum = 2;
outputs = 2;
}
if (n.op == "Slice") {
minimum = 3;
maximum = 5;
}
if (n.op == "Concat") maximum = 100000;
if (n.op == "Where") minimum = maximum = 3;
if (n.op == "Expand" || n.op == "Equal" || n.op == "Less" || n.op == "Greater" || n.op == "And")
minimum = maximum = 2;
if (n.op == "Constant") minimum = maximum = 0;
if (n.op == "DynamicQuantizeLinear") outputs = 3;
if (n.op == "QuantizeLinear" || n.op == "DequantizeLinear") {
minimum = 2;
maximum = 3;
}
if (n.op == "MatMulInteger" || n.op == "ConvInteger") {
minimum = 2;
maximum = 4;
}
if (n.op == "Gather" || n.op == "Reshape" || n.op == "Unsqueeze" || n.op == "MatMul" || n.op == "Add" ||
n.op == "Sub" || n.op == "Mul" || n.op == "Div" || n.op == "Pow")
minimum = maximum = 2;
if (n.op == "Squeeze" || n.op == "ReduceSum") maximum = 2;
if (n.inputs.size() < minimum || n.inputs.size() > maximum || n.outputs.size() != outputs) return false;
for (const auto& [key, a] : n.attrs) {
if (!op->second.contains(key)) return false;
const int expected = (key == "alpha" || key == "epsilon" || key == "cubic_coeff_a" || key == "seed" ||
key == "low" || key == "high" || key == "mean" || key == "scale")
? 1
: key == "value" ? 4
: (key == "auto_pad" || key == "reduction" || key == "coordinate_transformation_mode" ||
key == "mode" || key == "nearest_mode")
? 3
: (key == "perm" || key == "axes" || key == "dilations" || key == "kernel_shape" ||
key == "pads" || key == "strides" || key == "output_padding")
? 7
: 2;
if (a.type != expected) return false;
if (key == "reduction" && a.text != "none") return false;
if (key == "auto_pad" && a.text != "NOTSET") return false;
if (key == "allowzero" && a.integer != 0) return false;
}
if (n.op == "Pad" && n.attrs.contains("mode") && n.attrs.at("mode").text != "constant" &&
n.attrs.at("mode").text != "edge" && n.attrs.at("mode").text != "reflect")
return false;
if (n.op == "Resize") {
const auto mode = n.attrs.contains("mode") ? n.attrs.at("mode").text : "nearest";
const auto coordinate = n.attrs.contains("coordinate_transformation_mode")
? n.attrs.at("coordinate_transformation_mode").text
: "half_pixel";
const auto nearest = n.attrs.contains("nearest_mode") ? n.attrs.at("nearest_mode").text : "round_prefer_floor";
if (mode == "nearest") {
if (coordinate != "asymmetric" || nearest != "floor") return false;
} else if (mode != "linear" || (coordinate != "half_pixel" && coordinate != "asymmetric"))
return false;
}
return true;
}
namespace {
std::vector<int> dims(const RuntimeTensor& x) {
std::vector<int> out;
for (auto d : x.shape) {
if (d <= 0) throw Failure("Empty float kernel input", DiagnosticCode::Unsupported);
out.push_back(static_cast<int>(d));
}
return out;
}
std::vector<RuntimeTensor> single(RuntimeTensor x) { return {std::move(x)}; }
} // namespace
std::vector<RuntimeTensor> execute(const Node& n, const std::vector<const RuntimeTensor*>& in, OnnxCompute* compute) {
if (n.domain == "com.microsoft" && n.op == "DynamicQuantizeLSTM") return executeQuantLstm(n, in, compute);
if (n.op == "Constant") {
auto a = n.attrs.find("value");
if (a == n.attrs.end() || !a->second.tensor) throw Failure("Constant requires tensor value");
return single(*a->second.tensor);
}
if (n.op.find("Quantize") != std::string::npos || n.op == "DequantizeLinear" || n.op == "MatMulInteger" ||
n.op == "ConvInteger")
return executeQuant(n, in, compute);
const auto& x = required(in, 0);
if (n.op == "Identity") return single(x);
if (n.op == "Shape") {
const int64_t rank = x.shape.size();
auto norm = [rank](int64_t i) { return std::clamp(i < 0 ? i + rank : i, int64_t(0), rank); };
const auto begin = norm(attr(n, "start", 0)), end = norm(attr(n, "end", rank));
std::vector<int64_t> shape;
if (begin < end) shape.assign(x.shape.begin() + begin, x.shape.begin() + end);
return single(make(OnnxElement::Int64, {static_cast<int64_t>(shape.size())}, shape));
}
if (n.op == "Cast") {
const auto target = static_cast<OnnxElement>(attr(n, "to", 0));
if (compute && target == OnnxElement::Float32 && x.element == OnnxElement::Int32)
return single(dispatchFloat(*compute, {&x}, x.shape, "y[i]=float(x0[i]);"));
const auto size = elementSize(target);
RuntimeTensor out{target, x.shape, std::vector<uint8_t>(count(x.shape) * size)};
for (size_t i = 0; i < count(x.shape); ++i) {
if (target == OnnxElement::Float32) {
const float v =
x.element == OnnxElement::Float32 ? read<float>(x, i) : static_cast<float>(integer(x, i));
std::memcpy(out.bytes.data() + i * 4, &v, 4);
} else {
int64_t v;
if (x.element == OnnxElement::Float32) {
const double f = read<float>(x, i);
if (!std::isfinite(f) || f < -9223372036854775808.0 || f >= 9223372036854775808.0)
throw Failure("Cast overflow");
v = target == OnnxElement::Bool ? (f != 0) : static_cast<int64_t>(f);
} else
v = integer(x, i);
if (target == OnnxElement::Bool) v = v != 0;
std::memcpy(out.bytes.data() + i * size, &v, size);
}
}
return single(std::move(out));
}
if (n.op == "Gather") {
const auto& idx = required(in, 1);
const int a = axis(attr(n, "axis", 0), x.shape.size());
if (idx.element != OnnxElement::Int32 && idx.element != OnnxElement::Int64)
throw Failure("Gather indices must be int32/int64");
size_t inner = 1, outer = 1;
for (size_t i = a + 1; i < x.shape.size(); ++i) inner *= x.shape[i];
for (int i = 0; i < a; ++i) outer *= x.shape[i];
std::vector<int64_t> shape(x.shape.begin(), x.shape.begin() + a);
shape.insert(shape.end(), idx.shape.begin(), idx.shape.end());
shape.insert(shape.end(), x.shape.begin() + a + 1, x.shape.end());
const size_t stride = inner * elementSize(x.element), ni = count(idx.shape);
RuntimeTensor out{x.element, shape, std::vector<uint8_t>(count(shape) * elementSize(x.element))};
for (size_t o = 0; o < outer; ++o)
for (size_t i = 0; i < ni; ++i) {
auto j = integer(idx, i);
if (j < 0) j += x.shape[a];
if (j < 0 || j >= x.shape[a]) throw Failure("Gather index out of range");
if (stride)
std::memcpy(out.bytes.data() + (o * ni + i) * stride,
x.bytes.data() + (o * x.shape[a] + j) * stride, stride);
}
return single(std::move(out));
}
if (n.op == "Reshape" || n.op == "Unsqueeze" || n.op == "Squeeze") {
auto shape = x.shape;
if (n.op == "Reshape") {
shape = ints(required(in, 1));
int infer = -1;
size_t known = 1;
for (size_t i = 0; i < shape.size(); ++i) {
if (shape[i] == 0) {
if (i >= x.shape.size()) throw Failure("Reshape zero axis out of range");
shape[i] = x.shape[i];
}
if (shape[i] == -1) {
if (infer != -1) throw Failure("Multiple inferred dimensions");
infer = static_cast<int>(i);
} else {
if (shape[i] < 0 || shape[i] > INT32_MAX ||
(shape[i] && known > 128u * 1024u * 1024u / static_cast<size_t>(shape[i])))
throw Failure("Reshape overflow");
known *= static_cast<size_t>(shape[i]);
}
}
if (infer >= 0) {
if (!known || count(x.shape) % known) throw Failure("Invalid inferred shape");
shape[infer] = count(x.shape) / known;
}
} else {
std::vector<int64_t> axes;
if (in.size() > 1 && in[1])
axes = ints(*in[1]);
else if (n.op == "Unsqueeze")
throw Failure("Unsqueeze needs axes");
else
for (size_t i = 0; i < shape.size(); ++i)
if (shape[i] == 1) axes.push_back(i);
const size_t rank = shape.size() + (n.op == "Unsqueeze" ? axes.size() : 0);
std::set<int> positions;
for (auto a : axes)
if (!positions.insert(axis(a, rank)).second) throw Failure("Duplicate squeeze axis");
if (n.op == "Unsqueeze") {
for (int a : positions) shape.insert(shape.begin() + a, 1);
} else
for (auto it = positions.rbegin(); it != positions.rend(); ++it) {
if (shape[*it] != 1) throw Failure("Squeeze dimension not one");
shape.erase(shape.begin() + *it);
}
}
if (count(shape) != count(x.shape)) throw Failure("Reshape element count mismatch");
RuntimeTensor out = x;
out.shape = std::move(shape);
return single(std::move(out));
}
if (n.op == "Transpose") {
std::vector<int64_t> order = attrs(n, "perm", {});
if (order.empty())
for (size_t i = x.shape.size(); i > 0; --i) order.push_back(i - 1);
if (order.size() != x.shape.size()) throw Failure("Invalid transpose rank");
std::set<int64_t> used;
std::vector<int64_t> shape;
for (auto a : order) {
if (a < 0 || a >= static_cast<int64_t>(x.shape.size()) || !used.insert(a).second)
throw Failure("Invalid transpose permutation");
shape.push_back(x.shape[a]);
}
RuntimeTensor out{x.element, shape, std::vector<uint8_t>(x.bytes.size())};
const auto size = elementSize(x.element);
std::vector<size_t> strides(x.shape.size(), 1);
for (size_t i = x.shape.size(); i > 1; --i) strides[i - 2] = strides[i - 1] * x.shape[i - 1];
for (size_t i = 0; i < count(shape); ++i) {
size_t rest = i, source = 0;
for (size_t j = shape.size(); j > 0; --j) {
source += (rest % shape[j - 1]) * strides[order[j - 1]];
rest /= shape[j - 1];
}
std::memcpy(out.bytes.data() + i * size, x.bytes.data() + source * size, size);
}
return single(std::move(out));
}
if (auto indexResult = executeIndex(n, in)) return std::move(*indexResult);
if (auto shapeResult = executeShape(n, in)) return single(std::move(*shapeResult));
if (auto miscResult = executeMisc(n, in, compute)) return single(std::move(*miscResult));
if (auto neuralResult = executeNeural(n, in, compute)) return single(std::move(*neuralResult));
if (auto numericResult = executeNumeric(n, in, compute)) return single(std::move(*numericResult));
const auto a = floats(x);
if (n.op == "Softmax") {
auto xd = dims(x);
std::vector<float> out(a.size());
kernels::softmax(a.data(), xd.data(), static_cast<int>(xd.size()), axis(attr(n, "axis", -1), xd.size()), false,
out.data());
return single(make(OnnxElement::Float32, x.shape, out));
}
static const std::map<std::string, OpType> unary{
{"Sqrt", OpType::Sqrt}, {"Exp", OpType::Exp}, {"Log", OpType::Log}, {"Sin", OpType::Sin},
{"Cos", OpType::Cos}, {"Relu", OpType::Relu}, {"Sigmoid", OpType::Sigmoid}, {"Tanh", OpType::Tanh},
{"Neg", OpType::Neg}, {"Abs", OpType::Abs}};
auto op = unary.find(n.op);
if (op == unary.end()) throw Failure("Unsupported operator", DiagnosticCode::Unsupported);
std::vector<float> out(a.size());
kernels::unaryOp(op->second, a.data(), static_cast<int>(a.size()), out.data(), 0, 0);
return single(make(OnnxElement::Float32, x.shape, out));
}
} // namespace eve::tensor::onnx_detail
+378
View File
@@ -0,0 +1,378 @@
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <map>
#include <unordered_map>
#include "common/Subscription.h"
#include "gpgpu/ComputeShader.h"
#include "gpgpu/Gpgpu.h"
#include "gpgpu/GpuBuffer.h"
#include "gpgpu/Sequence.h"
#include "graphics/Graphics.h"
#include "tensor/OnnxCompiler.h"
#include "tensor/OnnxCompute.h"
namespace eve::tensor {
namespace {
using Clock = std::chrono::steady_clock;
struct Timed {
double& milliseconds;
Clock::time_point start = Clock::now();
~Timed() { milliseconds += std::chrono::duration<double, std::milli>(Clock::now() - start).count(); }
};
struct Run;
struct DeviceBytes final : OnnxDeviceStorage {
std::weak_ptr<Run> run;
std::unique_ptr<gpgpu::GpuBuffer> buffer;
size_t size = 0, capacity = 0;
uint64_t epoch = 0;
~DeviceBytes() override;
Result<std::vector<uint8_t>> readback() const override;
};
struct ShaderEntry {
std::future<onnx_detail::CompiledProgram> compilation;
std::unique_ptr<gpgpu::ComputeShader> shader;
};
struct PendingDispatch {
ShaderEntry* program;
std::vector<std::shared_ptr<DeviceBytes>> bindings;
uint32_t groups;
};
struct Run : std::enable_shared_from_this<Run> {
gpgpu::Gpgpu& gp;
std::unique_ptr<gpgpu::Sequence> sequence;
std::unordered_map<std::string, ShaderEntry> shaders;
onnx_detail::CompilerQueue compiler;
std::vector<PendingDispatch> dispatches;
double compileTaskMs = 0, compileWaitMs = 0, pipelineMs = 0;
std::map<std::shared_ptr<const std::vector<uint8_t>>, std::shared_ptr<DeviceBytes>,
std::owner_less<std::shared_ptr<const std::vector<uint8_t>>>>
uploads, persistentUploads;
std::vector<std::shared_ptr<const std::vector<uint8_t>>> uncommittedUploads;
bool active = true;
uint64_t epoch = 1;
std::vector<std::shared_ptr<DeviceBytes>> pending;
std::vector<std::weak_ptr<DeviceBytes>> resources;
OnnxTransferStats stats;
double shaderMs = 0, allocationMs = 0, uploadRecordMs = 0, dispatchRecordMs = 0, waitMs = 0, readbackMs = 0;
size_t compiledShaders = 0;
Clock::time_point started = Clock::now();
size_t queued = 0, cachedBytes = 0, allocatedBytes = 0;
std::multimap<size_t, std::unique_ptr<gpgpu::GpuBuffer>> freeBuffers;
explicit Run(gpgpu::Gpgpu& g, uint32_t workers) : gp(g), sequence(g.newSequence()), compiler(workers) {
sequence->begin();
}
~Run() {
// Destroy an unsubmitted command sequence before its referenced resources.
sequence.reset();
for (auto& [key, entry] : shaders)
if (entry.shader) entry.shader->clearBindings();
for (auto& weak : resources)
if (auto p = weak.lock()) p->buffer.reset();
}
void beginCall() noexcept {
stats = {};
compiler.resetPeak();
compileTaskMs = compileWaitMs = pipelineMs = 0;
shaderMs = allocationMs = uploadRecordMs = dispatchRecordMs = waitMs = readbackMs = 0;
compiledShaders = 0;
started = Clock::now();
active = true;
++epoch;
}
void endCall() noexcept {
compiler.waitIdle();
// An abandoned graph must not record or submit its deferred commands.
// Unmaterialized jobs contain only CPU data; discard their future (including any
// compilation exception) because the caller already received the graph failure.
dispatches.clear();
std::erase_if(shaders, [](const auto& item) { return !item.second.shader; });
if (const auto* profile = std::getenv("EVE_ONNX_PROFILE"); profile && std::string(profile) == "1")
std::cerr << "ONNX_PROFILE shader_ms=" << shaderMs << " allocation_ms=" << allocationMs
<< " upload_record_ms=" << uploadRecordMs << " dispatch_record_ms=" << dispatchRecordMs
<< " submit_wait_ms=" << waitMs << " readback_host_ms=" << readbackMs
<< " compile_task_ms=" << compileTaskMs << " compile_wait_ms=" << compileWaitMs
<< " pipeline_ms=" << pipelineMs << " compiler_peak_workers=" << compiler.peakWorkers()
<< " compiled_shaders=" << compiledShaders << " resident_scope_ms="
<< std::chrono::duration<double, std::milli>(Clock::now() - started).count() << '\n';
sequence.reset();
for (auto& [key, entry] : shaders)
if (entry.shader) entry.shader->clearBindings();
// Abandoned recorded uploads have not reached the device and cannot enter a persistent cache.
for (const auto& key : uncommittedUploads) {
if (auto it = persistentUploads.find(key); it != persistentUploads.end()) {
cachedBytes -= key->size();
persistentUploads.erase(it);
}
}
uncommittedUploads.clear();
pending.clear();
uploads.clear();
queued = 0;
active = false;
std::erase_if(resources, [](const auto& p) { return p.expired(); });
}
std::shared_ptr<DeviceBytes> allocate(size_t bytes) {
Timed timing{allocationMs};
if (!bytes || bytes > INT32_MAX - 3) throw std::runtime_error("Invalid resident buffer extent");
auto p = std::make_shared<DeviceBytes>();
p->run = shared_from_this();
p->size = bytes;
p->epoch = epoch;
const auto capacity = std::max(size_t(4), (bytes + 3) & ~size_t(3));
auto reusable = freeBuffers.lower_bound(capacity);
if (reusable != freeBuffers.end() && reusable->first <= std::max(size_t(4096), capacity * 2)) {
p->capacity = reusable->first;
p->buffer = std::move(reusable->second);
freeBuffers.erase(reusable);
++stats.bufferReuses;
} else {
if (allocatedBytes + capacity > 512u * 1024u * 1024u) {
for (const auto& [n, b] : freeBuffers) allocatedBytes -= n;
freeBuffers.clear();
}
if (allocatedBytes + capacity > 512u * 1024u * 1024u)
throw std::runtime_error("ONNX GPU resident memory limit exceeded");
p->buffer.reset(gp.newBuffer(static_cast<int>(capacity)));
p->capacity = capacity;
allocatedBytes += capacity;
++stats.bufferAllocations;
}
resources.push_back(p);
return p;
}
void recordDispatches() {
for (auto& command : dispatches) {
auto& entry = *command.program;
if (!entry.shader) {
Timed shaderTiming{shaderMs};
onnx_detail::CompiledProgram program;
{
Timed waitTiming{compileWaitMs};
program = entry.compilation.get();
}
compileTaskMs += program.milliseconds;
{
Timed pipelineTiming{pipelineMs};
auto result = gpgpu::createComputeShader(program.words);
if (!result.ok()) throw std::runtime_error(result.error()->message());
entry.shader = std::move(result.value());
}
}
Timed timing{dispatchRecordMs};
auto& shader = *entry.shader;
for (size_t i = 0; i < command.bindings.size(); ++i)
shader.bindBuffer(static_cast<int>(i), command.bindings[i]->buffer.get());
sequence->recordDispatch(&shader, static_cast<int>(command.groups));
shader.clearBindings();
}
stats.compilerPeakWorkers = compiler.peakWorkers();
dispatches.clear();
}
void flush() {
if (!queued) return;
recordDispatches();
Timed timing{waitMs};
// Submit asynchronously through the existing sequence contract; CPU visibility is
// requested only at a graph boundary. Retain every referenced allocation until wait.
if (sequence->submitAsync() != gpgpu::SequenceStatus::Submitted ||
sequence->wait() != gpgpu::SequenceStatus::Complete)
throw std::runtime_error("ONNX GPU submission failed");
uncommittedUploads.clear();
++stats.submissions;
pending.clear();
queued = 0;
sequence->begin();
}
std::shared_ptr<DeviceBytes> input(const OnnxBuffer& value) {
if (value.device) {
auto d = std::dynamic_pointer_cast<DeviceBytes>(value.device);
if (!d || d->run.lock().get() != this || !d->buffer || d->epoch != epoch || !active)
throw std::runtime_error("Foreign or expired GPU storage");
return d;
}
if (!value.host || value.host->size() != value.size) throw std::runtime_error("Invalid host input storage");
auto& cache = value.persistent ? persistentUploads : uploads;
if (auto it = cache.find(value.host); it != cache.end()) {
it->second->epoch = epoch;
return it->second;
}
if (value.persistent && cachedBytes + value.size > 128u * 1024u * 1024u) {
persistentUploads.clear();
cachedBytes = 0;
}
auto d = allocate(std::max(size_t(1), value.size));
Timed timing{uploadRecordMs};
std::vector<uint8_t> padded(std::max(size_t(4), (value.size + 3) & ~size_t(3)), 0);
std::copy(value.host->begin(), value.host->end(), padded.begin());
sequence->recordUpload(d->buffer.get(), padded.data(), padded.size());
++stats.uploads;
stats.uploadedBytes += value.size;
++queued;
cache.emplace(value.host, d);
if (value.persistent) {
cachedBytes += value.size;
uncommittedUploads.push_back(value.host);
}
pending.push_back(d);
return d;
}
};
DeviceBytes::~DeviceBytes() {
if (buffer)
if (auto owner = run.lock()) owner->freeBuffers.emplace(capacity, std::move(buffer));
}
Result<std::vector<uint8_t>> DeviceBytes::readback() const {
try {
auto owner = run.lock();
if (!owner || !buffer || !owner->active || epoch != owner->epoch)
throw std::runtime_error("ONNX GPU storage expired");
// Dispatches must precede the download in the command stream. Compilation
// waits are accounted separately from host readback and GPU submission.
owner->recordDispatches();
const auto start = Clock::now();
const auto priorWait = owner->waitMs;
std::unique_ptr<gpgpu::GpuBuffer> staging(owner->gp.newBuffer(static_cast<int>(size), "staging"));
owner->sequence->recordDownload(buffer.get(), staging.get(), size);
++owner->queued;
owner->flush();
std::vector<uint8_t> out(size);
staging->downloadBytes(out.data(), size);
owner->readbackMs +=
std::chrono::duration<double, std::milli>(Clock::now() - start).count() - (owner->waitMs - priorWait);
++owner->stats.downloads;
owner->stats.downloadedBytes += size;
return Result<std::vector<uint8_t>>::success(std::move(out));
} catch (const std::exception& e) {
return Result<std::vector<uint8_t>>::failure(Diagnostic::error(DiagnosticCode::Failed, e.what()));
}
}
struct SessionState {
std::shared_ptr<Run> run;
bool retired = false;
uint32_t compilerWorkers = 4;
};
class EngineCompute final : public OnnxCompute {
std::shared_ptr<SessionState> state;
eve::Subscription retirement;
OnnxTransferStats last;
bool active = false;
protected:
void beginRun() noexcept override {
if (state->run) state->run->beginCall();
last = {};
active = true;
}
void endRun() noexcept override {
if (state->run) {
last = state->run->stats;
state->run->endCall();
}
active = false;
}
public:
EngineCompute(std::shared_ptr<SessionState> s, eve::Subscription token)
: state(std::move(s)), retirement(std::move(token)) {}
OnnxTransferStats transferStats() const override { return active && state->run ? state->run->stats : last; }
Result<OnnxBuffer> enqueue(const std::string& source, std::span<const OnnxBuffer> inputs, size_t bytes,
uint32_t work) override {
try {
auto& run = state->run;
if (state->retired) throw std::runtime_error("ONNX GPU session device has been retired");
if (!active) throw std::runtime_error("Resident enqueue requires an active runGpu scope");
if (inputs.size() >= 8 || !bytes || !work || (uint64_t(work) + 63) / 64 > 65535)
throw std::runtime_error("Invalid GPU kernel extent");
if (!run) {
auto* gp = gpgpu::Gpgpu::create();
if (!gp || !gp->isAvailable()) throw std::runtime_error("ONNX GPU device unavailable");
run = std::make_shared<Run>(*gp, state->compilerWorkers);
}
if (!run->sequence) {
run->sequence.reset(run->gp.newSequence());
run->sequence->begin();
}
// Bound pending resources and the existing shader descriptor pool (64 sets).
if (run->queued >= 48) run->flush();
auto it = run->shaders.find(source);
if (it == run->shaders.end()) {
if (run->shaders.size() >= 1024) {
run->flush();
run->shaders.clear();
}
++run->stats.shaderCompilations;
++run->compiledShaders;
it = run->shaders.emplace(source, ShaderEntry{run->compiler.enqueue(source), {}}).first;
} else
++run->stats.shaderCacheHits;
PendingDispatch command{&it->second, {}, (work + 63) / 64};
for (const auto& input : inputs) {
auto d = run->input(input);
command.bindings.push_back(d);
run->pending.push_back(d);
}
auto out = run->allocate(bytes);
command.bindings.push_back(out);
run->dispatches.push_back(std::move(command));
run->pending.push_back(out);
++run->queued;
return Result<OnnxBuffer>::success({{}, out, bytes});
} catch (const std::exception& e) {
return Result<OnnxBuffer>::failure(
Diagnostic::error(DiagnosticCode::Failed, e.what(), {}, {}, "tensor.onnx.gpu"));
}
}
Result<std::vector<uint8_t>> dispatch(const OnnxKernel& k) override {
const bool temporary = !active;
if (temporary) beginRun();
struct Scope {
EngineCompute& owner;
bool temporary;
~Scope() {
if (temporary) owner.endRun();
}
} scope{*this, temporary};
std::vector<OnnxBuffer> inputs;
for (auto s : k.inputs) {
auto p = std::make_shared<const std::vector<uint8_t>>(s.begin(), s.end());
inputs.push_back({p, {}, p->size()});
}
auto r = enqueue(k.source, inputs, k.outputBytes, k.workItems);
if (!r.ok()) return Result<std::vector<uint8_t>>::failure(r.status());
return r.value().device->readback();
}
};
} // namespace
Result<std::unique_ptr<OnnxCompute>> createOnnxGpuCompute(uint32_t compilerWorkers) {
if (compilerWorkers < 1 || compilerWorkers > 8)
return Result<std::unique_ptr<OnnxCompute>>::failure(
Diagnostic::error(DiagnosticCode::InvalidArgument, "Compiler worker count must be 1..8"));
#ifdef EVENGINE_WEBGPU
return Result<std::unique_ptr<OnnxCompute>>::failure(
Diagnostic::error(DiagnosticCode::Unsupported, "ONNX GLSL kernels require the Vulkan backend"));
#else
try {
auto* gp = gpgpu::Gpgpu::create();
if (gp && gp->isAvailable()) {
auto state = std::make_shared<SessionState>();
state->compilerWorkers = compilerWorkers;
std::weak_ptr<SessionState> weak = state;
auto token = graphics::Graphics::create()->onResourcesRetiring([weak]() noexcept {
if (auto s = weak.lock()) {
s->retired = true;
s->run.reset();
}
});
if (!token.ok()) return Result<std::unique_ptr<OnnxCompute>>::failure(token.status());
return Result<std::unique_ptr<OnnxCompute>>::success(
std::make_unique<EngineCompute>(state, std::move(token.value())));
}
} catch (const std::exception& e) {
return Result<std::unique_ptr<OnnxCompute>>::failure(Diagnostic::error(DiagnosticCode::Failed, e.what()));
}
return Result<std::unique_ptr<OnnxCompute>>::failure(Diagnostic::error(
DiagnosticCode::Unsupported, "Initialize the engine Vulkan graphics device before ONNX GPU execution"));
#endif
}
} // namespace eve::tensor
+102
View File
@@ -0,0 +1,102 @@
#include "tensor/OnnxGpuKernels.h"
#include <sstream>
#include "tensor/OnnxInternal.h"
namespace eve::tensor::onnx_detail {
namespace {
std::string prefix(bool as, bool bs) {
std::ostringstream s;
s << "#version 450\nlayout(local_size_x=64) in;\n"
"layout(std430,binding=0) readonly buffer A {uint a[];};\n"
"layout(std430,binding=1) readonly buffer B {uint b[];};\n"
"layout(std430,binding=2) readonly buffer Z {int zp[];};\n"
"layout(std430,binding=3) writeonly buffer Y {int y[];};\n"
"int av(uint i){int v=int((a[i/4]>>((i%4)*8))&255u);return "
<< (as ? "v>=128?v-256:v" : "v")
<< ";}\n"
"int bv(uint i){int v=int((b[i/4]>>((i%4)*8))&255u);return "
<< (bs ? "v>=128?v-256:v" : "v") << ";}\n";
return s.str();
}
// Compatibility host-byte calls use the same resident kernel generators. The
// adapter below requests completion through the provider's synchronous contract.
class Synchronous final : public OnnxCompute {
OnnxCompute& target;
public:
explicit Synchronous(OnnxCompute& t) : target(t) {}
Result<std::vector<uint8_t>> dispatch(const OnnxKernel& k) override { return target.dispatch(k); }
};
OnnxBuffer hostBuffer(std::span<const uint8_t> bytes) {
auto p = std::make_shared<const std::vector<uint8_t>>(bytes.begin(), bytes.end());
return {p, {}, p->size()};
}
std::vector<int32_t> integerOutput(const OnnxBuffer& result) {
if (!result.host || result.host->size() % 4) throw Failure("Invalid synchronous integer output");
std::vector<int32_t> out(result.host->size() / 4);
std::memcpy(out.data(), result.host->data(), result.host->size());
return out;
}
} // namespace
std::vector<int32_t> gpuMatmul(OnnxCompute& d, affine::ByteView a, affine::ByteView b, size_t m, size_t k, size_t n,
int az, std::span<const int32_t> bz) {
Synchronous sync(d);
return integerOutput(gpuMatmulResident(sync, hostBuffer(a.bytes), hostBuffer(b.bytes), a.signedValues,
b.signedValues, m, k, n, az, bz));
}
std::vector<int32_t> gpuConv(OnnxCompute& d, affine::ByteView x, affine::ByteView w, const affine::ConvShape& c, int xz,
std::span<const int32_t> wz) {
Synchronous sync(d);
return integerOutput(
gpuConvResident(sync, hostBuffer(x.bytes), hostBuffer(w.bytes), x.signedValues, w.signedValues, c, xz, wz));
}
OnnxBuffer enqueueInteger(OnnxCompute& d, const std::string& source, OnnxBuffer a, OnnxBuffer b,
std::span<const int32_t> z, size_t size, size_t terms) {
if (terms > INT32_MAX / 65025 || !size || size > 64u * 65535u)
throw Failure("GPU integer kernel exceeds exact accumulator or dispatch limit", DiagnosticCode::Unsupported);
std::vector<uint8_t> bytes(z.size_bytes());
std::memcpy(bytes.data(), z.data(), bytes.size());
auto host = std::make_shared<const std::vector<uint8_t>>(std::move(bytes));
const std::vector<OnnxBuffer> inputs{a, b, {host, {}, host->size()}};
auto r = d.enqueue(source, inputs, size * 4, static_cast<uint32_t>(size));
if (!r.ok()) throw Failure(r.error()->message(), r.error()->code());
return std::move(r.value());
}
OnnxBuffer gpuMatmulResident(OnnxCompute& d, OnnxBuffer a, OnnxBuffer b, bool as, bool bs, size_t m, size_t k, size_t n,
int az, std::span<const int32_t> bz, size_t bOffset) {
if (bz.size() != 1 && bz.size() != n) throw Failure("GPU matmul zero-point extent mismatch");
std::ostringstream s;
s << prefix(as, bs) << "void main(){uint i=gl_GlobalInvocationID.x;if(i>=" << m * n << "u)return;"
<< "uint r=i/" << n << "u,c=i%" << n << "u;int v=0;for(uint j=0;j<" << k << "u;++j)"
<< "v+=(av(r*" << k << "u+j)-(" << az << "))*(bv(" << bOffset << "u+j*" << n << "u+c)-zp["
<< (bz.size() == 1 ? "0" : "c") << "]);y[i]=v;}";
return enqueueInteger(d, s.str(), a, b, bz, m * n, k);
}
OnnxBuffer gpuConvResident(OnnxCompute& d, OnnxBuffer x, OnnxBuffer w, bool xs, bool ws, const affine::ConvShape& c,
int xz, std::span<const int32_t> wz) {
const int64_t oh =
(int64_t(c.height) + c.padTop + c.padBottom - int64_t(c.dilationH) * (c.kernelH - 1) - 1) / c.strideH + 1;
const int64_t ow =
(int64_t(c.width) + c.padLeft + c.padRight - int64_t(c.dilationW) * (c.kernelW - 1) - 1) / c.strideW + 1;
if ((oh - 1) * c.strideH > INT32_MAX || (ow - 1) * c.strideW > INT32_MAX ||
(oh - 1) * c.strideH - c.padTop + int64_t(c.kernelH - 1) * c.dilationH > INT32_MAX ||
(ow - 1) * c.strideW - c.padLeft + int64_t(c.kernelW - 1) * c.dilationW > INT32_MAX)
throw Failure("GPU convolution coordinate overflow", DiagnosticCode::Unsupported);
const size_t size = size_t(c.batch) * c.outputs * oh * ow;
const int cg = c.channels / c.groups;
std::ostringstream s;
s << prefix(xs, ws) << "void main(){uint i=gl_GlobalInvocationID.x;if(i>=" << size << "u)return;"
<< "int xx=int(i%" << ow << "u),yy=int(i/" << ow << "u%" << oh << "u),o=int(i/" << ow * oh << "u%" << c.outputs
<< "u),batch=int(i/" << ow * oh * c.outputs << "u);int v=0;"
<< "for(int ch=0;ch<" << cg << ";++ch)for(int ky=0;ky<" << c.kernelH << ";++ky)for(int kx=0;kx<" << c.kernelW
<< ";++kx){"
<< "int iy=yy*" << c.strideH << "-" << c.padTop << "+ky*" << c.dilationH << ",ix=xx*" << c.strideW << "-"
<< c.padLeft << "+kx*" << c.dilationW << ";"
<< "if(iy<0||ix<0||iy>=" << c.height << "||ix>=" << c.width << ")continue;"
<< "uint ai=uint(((batch*" << c.channels << "+o/" << c.outputs / c.groups << "*" << cg << "+ch)*" << c.height
<< "+iy)*" << c.width << "+ix);"
<< "uint bi=uint(((o*" << cg << "+ch)*" << c.kernelH << "+ky)*" << c.kernelW << "+kx);"
<< "v+=(av(ai)-(" << xz << "))*(bv(bi)-zp[" << (wz.size() == 1 ? "0" : "o") << "]);}y[i]=v;}";
return enqueueInteger(d, s.str(), x, w, wz, size, size_t(cg) * c.kernelH * c.kernelW);
}
} // namespace eve::tensor::onnx_detail
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "tensor/AffineQuant.h"
#include "tensor/OnnxCompute.h"
namespace eve::tensor::onnx_detail {
// Internal kernels; callers validate geometry and byte extents before dispatch.
std::vector<int32_t> gpuMatmul(OnnxCompute& device, affine::ByteView a, affine::ByteView b, size_t m, size_t k,
size_t n, int az, std::span<const int32_t> bz);
std::vector<int32_t> gpuConv(OnnxCompute& device, affine::ByteView x, affine::ByteView w, const affine::ConvShape& s,
int xz, std::span<const int32_t> wz);
OnnxBuffer gpuMatmulResident(OnnxCompute&, OnnxBuffer, OnnxBuffer, bool, bool, size_t, size_t, size_t, int,
std::span<const int32_t>, size_t bOffset = 0);
OnnxBuffer gpuConvResident(OnnxCompute&, OnnxBuffer, OnnxBuffer, bool, bool, const affine::ConvShape&, int,
std::span<const int32_t>);
} // namespace eve::tensor::onnx_detail
+180
View File
@@ -0,0 +1,180 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
#include <set>
#include "tensor/OnnxInternal.h"
namespace eve::tensor::onnx_detail {
std::optional<std::vector<RuntimeTensor>> executeIndex(const Node& n, const std::vector<const RuntimeTensor*>& in) {
const auto& x = required(in, 0);
auto one = [](RuntimeTensor t) { return std::vector<RuntimeTensor>{std::move(t)}; };
if (n.op == "Range") {
const auto& end = required(in, 1);
const auto& delta = required(in, 2);
if (count(x.shape) != 1 || count(end.shape) != 1 || count(delta.shape) != 1 || x.element != end.element ||
x.element != delta.element)
throw Failure("Range requires matching scalars");
if (x.element == OnnxElement::Float32) {
const double start = read<float>(x, 0), stop = read<float>(end, 0), step = read<float>(delta, 0);
if (!std::isfinite(start) || !std::isfinite(stop) || !std::isfinite(step) || step == 0)
throw Failure("Invalid Range");
const double length = std::max(0., std::ceil((stop - start) / step));
if (length > 128u * 1024u * 1024u) throw Failure("Range too large");
std::vector<float> v(static_cast<size_t>(length));
for (size_t i = 0; i < v.size(); ++i) v[i] = static_cast<float>(start + i * step);
return one(make(x.element, {static_cast<int64_t>(v.size())}, v));
}
if (x.element != OnnxElement::Int64 && x.element != OnnxElement::Int32)
throw Failure("Range integer dtype unsupported");
const int64_t start = integer(x), stop = integer(end), step = integer(delta);
if (!step || step == INT64_MIN) throw Failure("Invalid Range step");
uint64_t length = 0;
if (step > 0 && stop > start) length = 1 + (uint64_t(stop) - uint64_t(start) - 1) / uint64_t(step);
if (step < 0 && stop < start) length = 1 + (uint64_t(start) - uint64_t(stop) - 1) / uint64_t(-step);
if (length > 128u * 1024u * 1024u) throw Failure("Range too large");
RuntimeTensor out{
x.element, {static_cast<int64_t>(length)}, std::vector<uint8_t>(length * elementSize(x.element))};
int64_t v = start;
for (size_t i = 0; i < length; ++i) {
std::memcpy(out.bytes.data() + i * elementSize(x.element), &v, elementSize(x.element));
if (i + 1 < length) v += step;
}
return one(std::move(out));
}
if (n.op == "TopK") {
const auto& kt = required(in, 1);
if (count(kt.shape) != 1) throw Failure("TopK requires one K");
int a = axis(attr(n, "axis", -1), x.shape.size());
const int64_t k = integer(kt), size = x.shape[a];
if (k < 0 || k > size) throw Failure("Invalid TopK K");
size_t outer = 1, inner = 1;
for (int j = 0; j < a; ++j) outer *= x.shape[j];
for (size_t j = a + 1; j < x.shape.size(); ++j) inner *= x.shape[j];
auto shape = x.shape;
shape[a] = k;
RuntimeTensor values{x.element, shape, std::vector<uint8_t>(count(shape) * elementSize(x.element))};
std::vector<int64_t> indices(count(shape));
for (size_t o = 0; o < outer; ++o)
for (size_t i = 0; i < inner; ++i) {
std::vector<int64_t> order(size);
std::iota(order.begin(), order.end(), 0);
std::stable_sort(order.begin(), order.end(), [&](int64_t aa, int64_t bb) {
auto compare = [&](auto u, auto v) { return attr(n, "largest", 1) ? u > v : u < v; };
const size_t ai = (o * size + aa) * inner + i, bi = (o * size + bb) * inner + i;
return x.element == OnnxElement::Float32 ? compare(read<float>(x, ai), read<float>(x, bi))
: compare(integer(x, ai), integer(x, bi));
});
for (int64_t j = 0; j < k; ++j) {
size_t dst = (o * k + j) * inner + i, src = (o * size + order[j]) * inner + i;
indices[dst] = order[j];
std::memcpy(values.bytes.data() + dst * elementSize(x.element),
x.bytes.data() + src * elementSize(x.element), elementSize(x.element));
}
}
return std::vector<RuntimeTensor>{std::move(values), make(OnnxElement::Int64, shape, indices)};
}
if (n.op == "ScatterElements" || n.op == "ScatterND") {
const auto& idx = required(in, 1);
const auto& updates = required(in, 2);
if (updates.element != x.element || (idx.element != OnnxElement::Int64 && idx.element != OnnxElement::Int32))
throw Failure("Scatter dtype mismatch");
auto out = x;
const auto bytes = elementSize(x.element);
if (n.op == "ScatterElements") {
if (idx.shape != updates.shape || idx.shape.size() != x.shape.size())
throw Failure("ScatterElements shape mismatch");
int a = axis(attr(n, "axis", 0), x.shape.size());
for (size_t j = 0; j < x.shape.size(); ++j)
if (j != a && idx.shape[j] > x.shape[j]) throw Failure("Scatter index shape exceeds data");
for (size_t i = 0; i < count(idx.shape); ++i) {
size_t rest = i, source = 0, stride = 1;
for (size_t j = x.shape.size(); j > 0; --j) {
int64_t c = rest % idx.shape[j - 1];
rest /= idx.shape[j - 1];
if (j - 1 == a) {
c = integer(idx, i);
if (c < 0) c += x.shape[j - 1];
}
if (c < 0 || c >= x.shape[j - 1]) throw Failure("Scatter index out of range");
source += c * stride;
stride *= x.shape[j - 1];
}
std::memcpy(out.bytes.data() + source * bytes, updates.bytes.data() + i * bytes, bytes);
}
} else {
if (idx.shape.empty() || idx.shape.back() < 1 || static_cast<size_t>(idx.shape.back()) > x.shape.size())
throw Failure("ScatterND index rank mismatch");
const size_t depth = idx.shape.back();
std::vector<int64_t> expected(idx.shape.begin(), idx.shape.end() - 1);
expected.insert(expected.end(), x.shape.begin() + depth, x.shape.end());
if (expected != updates.shape) throw Failure("ScatterND updates shape mismatch");
size_t chunk = 1;
for (size_t j = depth; j < x.shape.size(); ++j) chunk *= x.shape[j];
for (size_t i = 0; i < count(idx.shape) / depth; ++i) {
size_t target = 0;
for (size_t j = 0; j < depth; ++j) {
auto c = integer(idx, i * depth + j);
if (c < 0) c += x.shape[j];
if (c < 0 || c >= x.shape[j]) throw Failure("ScatterND index out of range");
target = target * x.shape[j] + c;
}
if (chunk)
std::memcpy(out.bytes.data() + target * chunk * bytes, updates.bytes.data() + i * chunk * bytes,
chunk * bytes);
}
}
return one(std::move(out));
}
if (n.op == "ReduceProd" || n.op == "ReduceMax") {
auto axes = attrs(n, "axes", {});
if (axes.empty())
for (size_t i = 0; i < x.shape.size(); ++i) axes.push_back(i);
std::set<int> reduced;
for (auto a : axes)
if (!reduced.insert(axis(a, x.shape.size())).second) throw Failure("Duplicate reduction axis");
auto mapShape = x.shape;
std::vector<int64_t> shape;
for (size_t j = 0; j < x.shape.size(); ++j) {
if (reduced.contains(static_cast<int>(j))) {
mapShape[j] = 1;
if (attr(n, "keepdims", 1)) shape.push_back(1);
} else
shape.push_back(x.shape[j]);
}
if (x.element == OnnxElement::Float32) {
std::vector<float> out(count(shape), n.op == "ReduceProd" ? 1.f : -std::numeric_limits<float>::infinity());
for (size_t i = 0; i < count(x.shape); ++i) {
size_t t = broadcastIndex(i, mapShape, x.shape);
const float v = read<float>(x, i);
out[t] = n.op == "ReduceProd" ? out[t] * v : std::max(out[t], v);
}
return one(make(x.element, shape, out));
}
if (x.element != OnnxElement::Int64 && x.element != OnnxElement::Int32)
throw Failure("Integer reduction dtype unsupported");
std::vector<int64_t> out(count(shape), n.op == "ReduceProd" ? 1 : INT64_MIN);
for (size_t i = 0; i < count(x.shape); ++i) {
auto& a = out[broadcastIndex(i, mapShape, x.shape)];
const auto b = integer(x, i);
if (n.op == "ReduceMax")
a = std::max(a, b);
else {
if (a && b &&
(a > 0 ? (b > 0 ? a > INT64_MAX / b : b < INT64_MIN / a)
: (b > 0 ? a < INT64_MIN / b : a < INT64_MAX / b)))
throw Failure("ReduceProd overflow");
a *= b;
}
}
if (x.element == OnnxElement::Int64) return one(make(x.element, shape, out));
std::vector<int32_t> narrow;
for (auto v : out) {
if (v < INT32_MIN || v > INT32_MAX) throw Failure("Int32 reduction overflow");
narrow.push_back(static_cast<int32_t>(v));
}
return one(make(x.element, shape, narrow));
}
return std::nullopt;
}
} // namespace eve::tensor::onnx_detail
+140
View File
@@ -0,0 +1,140 @@
#pragma once
#include <cstring>
#include <map>
#include <optional>
#include <stdexcept>
#include <unordered_map>
#include "tensor/OnnxByteStorage.h"
#include "tensor/OnnxModel.h"
namespace eve::tensor::onnx_detail {
struct Failure : std::runtime_error {
DiagnosticCode code;
std::string path;
explicit Failure(std::string message, DiagnosticCode c = DiagnosticCode::InvalidArgument, std::string p = {})
: std::runtime_error(std::move(message)), code(c), path(std::move(p)) {}
};
struct ModelData;
struct Attribute {
int type = 0;
int64_t integer = 0;
float real = 0;
std::string text;
std::vector<int64_t> integers;
std::vector<float> reals;
std::vector<std::string> strings;
std::optional<RuntimeTensor> tensor;
std::shared_ptr<ModelData> graph;
};
struct Node {
std::string name, op, domain;
std::vector<std::string> inputs, outputs;
std::map<std::string, Attribute> attrs;
};
struct Input {
int type = 0;
std::vector<int64_t> shape;
};
struct ModelData {
OnnxModelInfo info;
std::vector<Node> nodes;
std::unordered_map<std::string, RuntimeTensor> constants;
std::map<std::string, Input> inputs;
std::map<std::string, int64_t> opsets;
};
inline size_t elementSize(OnnxElement e) {
switch (e) {
case OnnxElement::Float32:
case OnnxElement::Int32: return 4;
case OnnxElement::Int64: return 8;
case OnnxElement::UInt8:
case OnnxElement::Int8:
case OnnxElement::Bool: return 1;
}
throw Failure("Unsupported ONNX element type", DiagnosticCode::Unsupported);
}
inline size_t count(const std::vector<int64_t>& shape) {
if (shape.size() > 6) throw Failure("ONNX rank exceeds six", DiagnosticCode::Unsupported);
size_t n = 1;
for (int64_t d : shape) {
if (d < 0 || d > INT32_MAX || (d && n > (128u * 1024u * 1024u) / static_cast<size_t>(d)))
throw Failure("Invalid or excessive tensor shape");
n *= static_cast<size_t>(d);
}
return n;
}
inline void validate(const RuntimeTensor& v) {
if (count(v.shape) * elementSize(v.element) != v.bytes.size()) throw Failure("Tensor byte count mismatch");
}
template <class T>
T read(const RuntimeTensor& v, size_t i) {
T x;
std::memcpy(&x, v.bytes.data() + i * sizeof(T), sizeof(T));
return x;
}
template <class T>
RuntimeTensor make(OnnxElement e, std::vector<int64_t> shape, const std::vector<T>& values) {
RuntimeTensor v{e, std::move(shape), std::vector<uint8_t>(values.size() * sizeof(T))};
if (!values.empty()) std::memcpy(v.bytes.data(), values.data(), v.bytes.size());
validate(v);
return v;
}
inline int64_t integer(const RuntimeTensor& v, size_t i = 0) {
if (i >= count(v.shape)) throw Failure("Integer index out of bounds");
switch (v.element) {
case OnnxElement::Int64: return read<int64_t>(v, i);
case OnnxElement::Int32: return read<int32_t>(v, i);
case OnnxElement::Int8: return v.bytes[i] >= 128 ? int(v.bytes[i]) - 256 : v.bytes[i];
case OnnxElement::UInt8:
case OnnxElement::Bool: return v.bytes[i];
default: throw Failure("Expected integer tensor");
}
}
inline std::vector<float> floats(const RuntimeTensor& v) {
if (v.element != OnnxElement::Float32) throw Failure("Expected FP32 tensor");
std::vector<float> out(count(v.shape));
if (!out.empty()) std::memcpy(out.data(), v.bytes.data(), v.bytes.size());
return out;
}
inline std::vector<int64_t> ints(const RuntimeTensor& v) {
std::vector<int64_t> out(count(v.shape));
for (size_t i = 0; i < out.size(); ++i) out[i] = integer(v, i);
return out;
}
inline int64_t attr(const Node& n, const char* key, int64_t fallback) {
auto it = n.attrs.find(key);
return it == n.attrs.end() ? fallback : it->second.integer;
}
inline std::vector<int64_t> attrs(const Node& n, const char* key, std::vector<int64_t> fallback) {
auto it = n.attrs.find(key);
return it == n.attrs.end() ? fallback : it->second.integers;
}
inline const RuntimeTensor& required(const std::vector<const RuntimeTensor*>& in, size_t i) {
if (i >= in.size() || !in[i]) throw Failure("Missing required input");
return *in[i];
}
inline int axis(int64_t a, size_t rank) {
if (a < 0) a += rank;
if (a < 0 || a >= static_cast<int64_t>(rank)) throw Failure("Invalid axis");
return static_cast<int>(a);
}
std::vector<int64_t> broadcast(const std::vector<int64_t>& a, const std::vector<int64_t>& b);
size_t broadcastIndex(size_t i, const std::vector<int64_t>& shape, const std::vector<int64_t>& output);
std::optional<std::vector<RuntimeTensor>> executeIndex(const Node&, const std::vector<const RuntimeTensor*>&);
std::optional<RuntimeTensor> executeShape(const Node&, const std::vector<const RuntimeTensor*>&);
RuntimeTensor dispatchFloat(OnnxCompute&, const std::vector<const RuntimeTensor*>&, const std::vector<int64_t>&,
const std::string&, size_t work = 0);
std::optional<RuntimeTensor> executeMisc(const Node&, const std::vector<const RuntimeTensor*>&, OnnxCompute*);
std::optional<RuntimeTensor> executeNeural(const Node&, const std::vector<const RuntimeTensor*>&, OnnxCompute*);
std::optional<RuntimeTensor> executeNumeric(const Node&, const std::vector<const RuntimeTensor*>&, OnnxCompute*);
std::vector<OnnxNamedTensor> evaluate(const ModelData&, std::span<const OnnxNamedTensor>,
const std::vector<std::string>&, OnnxCompute*, OnnxRunOptions options);
ModelData parse(std::span<const uint8_t> bytes);
bool isSupported(const Node& node);
std::vector<RuntimeTensor> execute(const Node& node, const std::vector<const RuntimeTensor*>& inputs,
OnnxCompute* compute = nullptr);
std::vector<RuntimeTensor> executeQuant(const Node& node, const std::vector<const RuntimeTensor*>& inputs,
OnnxCompute* compute = nullptr);
std::vector<RuntimeTensor> executeQuantLstm(const Node& node, const std::vector<const RuntimeTensor*>& inputs,
OnnxCompute* compute = nullptr);
} // namespace eve::tensor::onnx_detail
+142
View File
@@ -0,0 +1,142 @@
#include "tensor/AffineQuant.h"
#include "tensor/OnnxGpuKernels.h"
#include "tensor/OnnxInternal.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace eve::tensor::onnx_detail {
namespace {
template <class T>
T checked(Result<T> r) {
if (!r.ok()) throw Failure(r.error()->message(), r.error()->code());
return std::move(r.value());
}
void shape(const RuntimeTensor& t, const std::vector<int64_t>& expected) {
if (t.shape != expected) throw Failure("LSTM input shape mismatch");
}
std::vector<float> optionalFloats(const std::vector<const RuntimeTensor*>& in, size_t i,
const std::vector<int64_t>& dims) {
if (i >= in.size() || !in[i]) return std::vector<float>(count(dims), 0);
shape(*in[i], dims);
return floats(*in[i]);
}
struct Weights {
const RuntimeTensor& tensor;
std::vector<float> scales;
std::vector<int64_t> zeros;
size_t directions, gates, rows;
Weights(const RuntimeTensor& t, const RuntimeTensor& s, const RuntimeTensor& z, size_t d, size_t g, size_t r)
: tensor(t), scales(floats(s)), zeros(ints(z)), directions(d), gates(g), rows(r) {
if (t.element != OnnxElement::Int8 && t.element != OnnxElement::UInt8)
throw Failure("LSTM weights must be 8-bit");
if (t.element != z.element || s.shape != z.shape ||
(s.shape != std::vector<int64_t>{static_cast<int64_t>(d)} &&
s.shape != std::vector<int64_t>{static_cast<int64_t>(d), static_cast<int64_t>(g)}))
throw Failure("LSTM quantization parameter shape mismatch");
shape(t, {static_cast<int64_t>(d), static_cast<int64_t>(r), static_cast<int64_t>(g)});
for (float v : scales)
if (!(v > 0) || !std::isfinite(v)) throw Failure("Invalid LSTM weight scale");
}
std::vector<float> multiply(const affine::QuantizedActivation& a, size_t batch, size_t direction,
OnnxCompute* compute) const {
std::vector<float> out(batch * gates);
const bool sign = tensor.element == OnnxElement::Int8;
if (compute) {
std::vector<int32_t> z(scales.size() == directions ? 1 : gates);
for (size_t i = 0; i < z.size(); ++i) z[i] = static_cast<int32_t>(zeros[direction * z.size() + i]);
ByteStorage activation(a.values);
ByteStorage accumulator(gpuMatmulResident(*compute, activation.buffer(), tensor.bytes.buffer(), false, sign,
batch, rows, gates, a.zeroPoint, z, direction * rows * gates));
std::vector<int32_t> sums(batch * gates);
std::memcpy(sums.data(), static_cast<const ByteStorage&>(accumulator).data(),
sums.size() * sizeof(int32_t));
for (size_t b = 0; b < batch; ++b)
for (size_t g = 0; g < gates; ++g)
out[b * gates + g] = static_cast<float>(sums[b * gates + g]) * a.scale *
scales[direction * z.size() + (z.size() == 1 ? 0 : g)];
return out;
}
// Per-output-channel affine zero points do not require unpacking a weight matrix.
for (size_t b = 0; b < batch; ++b)
for (size_t g = 0; g < gates; ++g) {
const size_t p = scales.size() == directions ? direction : direction * gates + g;
int64_t sum = 0;
for (size_t k = 0; k < rows; ++k) {
const int byte = tensor.bytes[(direction * rows + k) * gates + g];
const int w = sign && byte >= 128 ? byte - 256 : byte;
sum += static_cast<int64_t>(int(a.values[b * rows + k]) - a.zeroPoint) * (w - zeros[p]);
}
if (sum < INT32_MIN || sum > INT32_MAX) throw Failure("LSTM integer accumulator overflow");
out[b * gates + g] = static_cast<float>(sum) * a.scale * scales[p];
}
return out;
}
};
} // namespace
std::vector<RuntimeTensor> executeQuantLstm(const Node& n, const std::vector<const RuntimeTensor*>& in,
OnnxCompute* compute) {
const auto& xt = required(in, 0);
if (xt.shape.size() != 3) throw Failure("LSTM X must be [time,batch,input]");
const auto x = floats(xt);
const int64_t time = xt.shape[0], batch = xt.shape[1], input = xt.shape[2], hidden = attr(n, "hidden_size", 0);
const auto direction = n.attrs.contains("direction") ? n.attrs.at("direction").text : "forward";
const int64_t dirs = direction == "bidirectional" ? 2 : 1;
if (time <= 0 || batch <= 0 || input <= 0 || hidden <= 0 || hidden > 16384)
throw Failure("Invalid LSTM dimensions");
const size_t gates = 4 * hidden;
const size_t projectionSize = count({time, batch, static_cast<int64_t>(gates)});
if (projectionSize > 128u * 1024u * 1024u) throw Failure("LSTM projection exceeds memory limit");
Weights w(required(in, 1), required(in, 8), required(in, 9), dirs, gates, input);
Weights r(required(in, 2), required(in, 10), required(in, 11), dirs, gates, hidden);
auto biases = optionalFloats(in, 3, {dirs, 8 * hidden});
auto h = optionalFloats(in, 5, {dirs, batch, hidden}), c = optionalFloats(in, 6, {dirs, batch, hidden});
auto p = optionalFloats(in, 7, {dirs, 3 * hidden});
std::vector<int64_t> lengths(batch, time);
if (in.size() > 4 && in[4]) {
if (in[4]->element != OnnxElement::Int32) throw Failure("LSTM sequence lengths require int32");
shape(*in[4], {batch});
lengths = ints(*in[4]);
}
for (auto len : lengths)
if (len < 0 || len > time) throw Failure("Invalid LSTM sequence length");
const int64_t coupled = attr(n, "input_forget", 0);
if (coupled != 0 && coupled != 1) throw Failure("Invalid coupled gate flag");
const float clip = n.attrs.contains("clip") ? n.attrs.at("clip").real : std::numeric_limits<float>::max();
if (!(clip > 0) || !std::isfinite(clip)) throw Failure("Invalid LSTM clipping threshold");
auto sigmoid = [clip](float z) { return 1.f / (1.f + std::exp(-std::clamp(z, -clip, clip))); };
auto tanh = [clip](float z) { return std::tanh(std::clamp(z, -clip, clip)); };
std::vector<float> y(count({time, dirs, batch, hidden}), 0);
const auto qx = checked(affine::dynamicQuantize(x));
for (int64_t d = 0; d < dirs; ++d) {
const auto projected = w.multiply(qx, time * batch, d, compute);
const bool reverse = direction == "reverse" || d == 1;
for (int64_t step = 0; step < time; ++step) {
const auto qh = checked(affine::dynamicQuantize(std::span(h).subspan(d * batch * hidden, batch * hidden)));
const auto recurrent = r.multiply(qh, batch, d, compute);
for (int64_t b = 0; b < batch; ++b) {
if (step >= lengths[b]) continue;
const int64_t t = reverse ? lengths[b] - 1 - step : step;
const size_t state = (d * batch + b) * hidden;
for (int64_t j = 0; j < hidden; ++j) {
auto gate = [&](int g) {
const size_t channel = g * hidden + j;
return projected[(t * batch + b) * gates + channel] + recurrent[b * gates + channel] +
biases[d * 8 * hidden + channel] + biases[d * 8 * hidden + gates + channel];
};
const float i = sigmoid(gate(0) + p[d * 3 * hidden + j] * c[state + j]);
const float f =
coupled ? 1 - i : sigmoid(gate(2) + p[d * 3 * hidden + 2 * hidden + j] * c[state + j]);
c[state + j] = f * c[state + j] + i * tanh(gate(3));
const float o = sigmoid(gate(1) + p[d * 3 * hidden + hidden + j] * c[state + j]);
h[state + j] = o * tanh(c[state + j]);
y[((t * dirs + d) * batch + b) * hidden + j] = h[state + j];
}
}
}
}
return {make(OnnxElement::Float32, {time, dirs, batch, hidden}, y),
make(OnnxElement::Float32, {dirs, batch, hidden}, h), make(OnnxElement::Float32, {dirs, batch, hidden}, c)};
}
} // namespace eve::tensor::onnx_detail
+107
View File
@@ -0,0 +1,107 @@
#include <algorithm>
#include <limits>
#include <sstream>
#include "tensor/OnnxInternal.h"
namespace eve::tensor::onnx_detail {
std::optional<RuntimeTensor> executeMisc(const Node& n, const std::vector<const RuntimeTensor*>& in,
OnnxCompute* compute) {
const auto& x = required(in, 0);
if (n.op == "Clip") {
if (x.element != OnnxElement::Float32)
throw Failure("Clip currently requires FP32", DiagnosticCode::Unsupported);
auto low = make(x.element, {}, std::vector<float>{-std::numeric_limits<float>::infinity()}),
high = make(x.element, {}, std::vector<float>{std::numeric_limits<float>::infinity()});
const auto& a = in.size() > 1 && in[1] ? *in[1] : low;
const auto& b = in.size() > 2 && in[2] ? *in[2] : high;
if (a.element != x.element || b.element != x.element || count(a.shape) != 1 || count(b.shape) != 1)
throw Failure("Clip bounds must be matching scalars");
if (compute) return dispatchFloat(*compute, {&x, &a, &b}, x.shape, "y[i]=min(max(x0[i],x1[0]),x2[0]);");
auto v = floats(x);
for (auto& f : v) f = std::min(std::max(f, read<float>(a, 0)), read<float>(b, 0));
return make(x.element, x.shape, v);
}
if (n.op == "CumSum") {
const auto& at = required(in, 1);
if (count(at.shape) != 1) throw Failure("CumSum axis must be scalar");
const int a = axis(integer(at), x.shape.size());
if (x.element != OnnxElement::Float32)
throw Failure("CumSum currently requires FP32", DiagnosticCode::Unsupported);
const bool reverse = attr(n, "reverse", 0) != 0, exclusive = attr(n, "exclusive", 0) != 0;
size_t outer = 1, inner = 1;
for (int j = 0; j < a; ++j) outer *= x.shape[j];
for (size_t j = a + 1; j < x.shape.size(); ++j) inner *= x.shape[j];
const auto width = x.shape[a];
if (compute && outer && inner && width) {
std::ostringstream s;
s << "uint base=i/" << inner << "u*" << inner * width << "u+i%" << inner
<< "u;precise float sum=0.0;for(uint j=0;j<" << width << "u;++j){uint index=base+"
<< (reverse ? "(" + std::to_string(width - 1) + "u-j)" : "j") << "*" << inner << "u;"
<< (exclusive ? "y[index]=sum;sum+=x0[index];" : "sum+=x0[index];y[index]=sum;") << "}";
return dispatchFloat(*compute, {&x}, x.shape, s.str(), outer * inner);
}
auto out = floats(x);
for (size_t o = 0; o < outer; ++o)
for (size_t i = 0; i < inner; ++i) {
float sum = 0;
for (int64_t j = 0; j < width; ++j) {
size_t p = (o * width + (reverse ? width - 1 - j : j)) * inner + i;
const float v = out[p];
if (exclusive) {
out[p] = sum;
sum += v;
} else {
sum += v;
out[p] = sum;
}
}
}
return make(x.element, x.shape, out);
}
if (n.op == "Pad") {
const auto pads = ints(required(in, 1));
if (pads.size() != 2 * x.shape.size()) throw Failure("Pad rank mismatch");
auto shape = x.shape;
const auto mode = n.attrs.contains("mode") ? n.attrs.at("mode").text : "constant";
for (size_t j = 0; j < shape.size(); ++j) {
if (pads[j] < -INT32_MAX || pads[j] > INT32_MAX || pads[j + shape.size()] < -INT32_MAX ||
pads[j + shape.size()] > INT32_MAX)
throw Failure("Pad extent out of range");
shape[j] += pads[j] + pads[j + shape.size()];
if (mode != "constant" && x.shape[j] <= 0)
throw Failure("Edge/reflect padding requires nonempty dimensions");
if (mode == "reflect" && (pads[j] >= x.shape[j] || pads[j + shape.size()] >= x.shape[j]))
throw Failure("Reflect padding exceeds input dimension");
}
const auto size = elementSize(x.element);
std::vector<uint8_t> fill(size, 0);
if (in.size() > 2 && in[2]) {
if (in[2]->element != x.element || count(in[2]->shape) != 1)
throw Failure("Pad value dtype/shape mismatch");
fill = in[2]->bytes;
}
RuntimeTensor out{x.element, shape, std::vector<uint8_t>(count(shape) * size)};
for (size_t i = 0; i < count(shape); ++i) {
size_t rest = i, source = 0, stride = 1;
bool outside = false;
for (size_t j = shape.size(); j > 0; --j) {
int64_t p = static_cast<int64_t>(rest % shape[j - 1]) - pads[j - 1];
rest /= shape[j - 1];
if (p < 0 || p >= x.shape[j - 1]) {
if (mode == "constant")
outside = true;
else if (mode == "edge")
p = std::clamp(p, int64_t(0), x.shape[j - 1] - 1);
else
p = p < 0 ? -p : 2 * x.shape[j - 1] - 2 - p;
}
if (!outside) source += p * stride;
stride *= x.shape[j - 1];
}
std::memcpy(out.bytes.data() + i * size, outside ? fill.data() : x.bytes.data() + source * size, size);
}
return out;
}
return std::nullopt;
}
} // namespace eve::tensor::onnx_detail
+94
View File
@@ -0,0 +1,94 @@
#include "tensor/OnnxCompute.h"
#include "tensor/OnnxInternal.h"
#include <set>
namespace eve::tensor {
struct OnnxModel::Impl {
onnx_detail::ModelData model;
};
OnnxModel::OnnxModel(std::unique_ptr<Impl> impl) : impl_(std::move(impl)) {}
OnnxModel::~OnnxModel() = default;
OnnxModelInfo OnnxModel::info() const { return impl_->model.info; }
Result<std::unique_ptr<OnnxModel>> OnnxModel::load(std::span<const uint8_t> bytes) {
try {
auto impl = std::make_unique<Impl>();
impl->model = onnx_detail::parse(bytes);
return Result<std::unique_ptr<OnnxModel>>::success(std::unique_ptr<OnnxModel>(new OnnxModel(std::move(impl))));
} catch (const onnx_detail::Failure& e) {
return Result<std::unique_ptr<OnnxModel>>::failure(
Diagnostic::error(e.code, e.what(), {}, {}, "tensor.onnx.import"));
} catch (const std::exception& e) {
return Result<std::unique_ptr<OnnxModel>>::failure(
Diagnostic::error(DiagnosticCode::Failed, e.what(), {}, {}, "tensor.onnx.import"));
}
}
Result<std::vector<OnnxNamedTensor>> OnnxModel::run(std::span<const OnnxNamedTensor> feeds,
std::span<const std::string> requested,
OnnxRunOptions options) const {
return runInternal(feeds, requested, nullptr, options);
}
Result<OnnxGpuResult> OnnxModel::runGpu(std::span<const OnnxNamedTensor> feeds, OnnxCompute& compute,
std::span<const std::string> requested, OnnxRunOptions options) const {
class Counter final : public OnnxCompute {
public:
OnnxCompute& target;
size_t count = 0;
explicit Counter(OnnxCompute& t) : target(t) {}
Result<std::vector<uint8_t>> dispatch(const OnnxKernel& k) override {
auto r = target.dispatch(k);
if (r.ok()) ++count;
return r;
}
Result<OnnxBuffer> enqueue(const std::string& source, std::span<const OnnxBuffer> inputs, size_t bytes,
uint32_t work) override {
auto r = target.enqueue(source, inputs, bytes, work);
if (r.ok()) ++count;
return r;
}
} counter(compute);
compute.beginRun();
struct RunScope {
OnnxCompute& device;
~RunScope() { device.endRun(); }
} scope{compute};
auto r = runInternal(feeds, requested, &counter, options);
if (!r.ok()) return Result<OnnxGpuResult>::failure(r.status());
return Result<OnnxGpuResult>::success({std::move(r.value()), counter.count, compute.transferStats()});
}
Result<std::vector<OnnxNamedTensor>> OnnxModel::runInternal(std::span<const OnnxNamedTensor> feeds,
std::span<const std::string> requested,
OnnxCompute* compute, OnnxRunOptions options) const {
using namespace onnx_detail;
std::string nodeName;
try {
const auto& model = impl_->model;
const std::vector<std::string> targets =
requested.empty() ? model.info.outputs : std::vector<std::string>(requested.begin(), requested.end());
std::set<std::string> seenFeeds;
for (const auto& f : feeds) {
nodeName = f.name;
if (count(f.tensor.shape) * elementSize(f.tensor.element) != f.tensor.bytes.size())
throw Failure("Tensor byte count mismatch");
auto input = model.inputs.find(f.name);
if (input == model.inputs.end() || !seenFeeds.insert(f.name).second)
throw Failure("Unknown or duplicate feed");
if (static_cast<int>(f.tensor.element) != input->second.type ||
f.tensor.shape.size() != input->second.shape.size())
throw Failure("Feed type/rank mismatch");
for (size_t i = 0; i < f.tensor.shape.size(); ++i)
if (input->second.shape[i] >= 0 && input->second.shape[i] != f.tensor.shape[i])
throw Failure("Feed dimension mismatch");
}
nodeName.clear();
auto result = evaluate(model, feeds, targets, compute, options);
return Result<std::vector<OnnxNamedTensor>>::success(std::move(result));
} catch (const Failure& e) {
return Result<std::vector<OnnxNamedTensor>>::failure(
Diagnostic::error(e.code, e.what(), e.path.empty() ? nodeName : e.path, {}, "tensor.onnx.run"));
} catch (const std::exception& e) {
return Result<std::vector<OnnxNamedTensor>>::failure(
Diagnostic::error(DiagnosticCode::Failed, e.what(), nodeName, {}, "tensor.onnx.run"));
}
}
} // namespace eve::tensor
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include "common/Result.h"
#include "tensor/OnnxStorage.h"
#include <cstddef>
#include <cstdint>
#include <memory>
#include <span>
#include <string>
#include <vector>
namespace eve::tensor {
class OnnxCompute;
/** @brief ONNX wire element types; distinct from block-quantized Tensor storage. */
enum class OnnxElement : int { Float32 = 1, UInt8 = 2, Int8 = 3, Int32 = 6, Int64 = 7, Bool = 9 };
/**
* @brief Owning ONNX boundary tensor: row-major little-endian bytes and exact integer shape.
* @note No borrowed storage. run() validates caller-provided values before execution.
* Scalar shape is empty; zero-sized dimensions are supported. No implicit FP32 integer conversion.
*/
struct OnnxTensor {
OnnxElement element = OnnxElement::Float32;
std::vector<int64_t> shape;
std::vector<uint8_t> bytes;
};
/** @brief Owning named feed/output value. */
struct OnnxNamedTensor {
std::string name;
OnnxTensor tensor;
};
/** @brief Per-call deterministic RNG and optional strict finite-output diagnostic.
* @note requireFinite rejects even intentional intermediate infinities/NaNs, so keep it false for Kokoro. */
struct OnnxRunOptions {
uint64_t seed = 0;
bool requireFinite = false;
};
/** @brief Owning GPU execution output and completed dispatch count. */
struct OnnxGpuResult {
std::vector<OnnxNamedTensor> outputs;
size_t dispatches = 0;
OnnxTransferStats transfers;
};
/** @brief Owning admission report; unsupported nodes remain inspectable but cannot execute. */
struct OnnxModelInfo {
int64_t irVersion = 0;
size_t nodeCount = 0, initializerBytes = 0;
std::vector<std::string> inputs, outputs;
std::vector<std::string> unsupportedNodes;
};
/**
* @brief Native ONNX import and CPU/GPU execution using tensor kernels, without ONNX Runtime.
* @note Owns immutable model data. Concurrent CPU run calls are independent; no script callbacks,
* global RNG or background threads. runGpu uses a caller-provided device on its owning thread.
* FP32 results use tolerance comparison. Seeded random excitation is local to a run.
* Loading is transactional. Unknown protobuf metadata is ignored, unknown operators
* are reported; unsupported semantic features fail explicitly, never become identity.
*/
class OnnxModel {
public:
/** @brief Destroy model-owned graph and packed initializers; outputs remain valid. */
~OnnxModel();
/**
* @brief Import bounded in-memory ONNX ModelProto; copies all retained data.
* @param bytes Borrowed input, not retained. Maximum 512 MiB, 100000 recursive nodes, graph depth 16, rank 6.
* @return Owning model or ParseError/Unsupported/UnknownVersion; no partial publication.
* @note Supports ONNX IR 3..10 and default-domain opset 13..17. External data is rejected.
*/
[[nodiscard]] static Result<std::unique_ptr<OnnxModel>> load(std::span<const uint8_t> bytes);
/** @brief Return owning graph diagnostics; importing does not imply full operator support. */
[[nodiscard]] OnnxModelInfo info() const;
/**
* @brief Execute selected named values, or graph outputs when requested is empty.
* @param feeds Borrowed inputs, valid only during this call; never mutated or retained.
* @param requested Borrowed output names; intermediate outputs may be selected for parity tests.
* @return Owning outputs or structured node diagnostic; failed calls publish no outputs.
* @note CPU only. Unsupported dependencies fail before executing the selected subgraph.
*/
[[nodiscard]] Result<std::vector<OnnxNamedTensor>> run(std::span<const OnnxNamedTensor> feeds,
std::span<const std::string> requested = {},
OnnxRunOptions options = {}) const;
/**
* @brief Execute with GPU matrix, convolution, normalization, activation and resampling kernels.
* @note Shape/index/control operations, padding and LSTM gates execute on CPU.
* Dynamic quantization reads three validation scalars; activations remain on GPU.
* Integer GEMM/Conv, LSTM projections, FP32 GEMM/ConvTranspose, elementwise math,
* Softmax, sum/mean reductions, normalization, Resize and CumSum execute on GPU.
* The borrowed compute provider must outlive this synchronous call; use its device thread.
* No automatic CPU retry occurs after a GPU error. Outputs own their storage.
*/
[[nodiscard]] Result<OnnxGpuResult> runGpu(std::span<const OnnxNamedTensor> feeds, OnnxCompute& compute,
std::span<const std::string> requested = {},
OnnxRunOptions options = {}) const;
private:
[[nodiscard]] Result<std::vector<OnnxNamedTensor>> runInternal(std::span<const OnnxNamedTensor> feeds,
std::span<const std::string> requested,
OnnxCompute* compute, OnnxRunOptions options) const;
struct Impl;
explicit OnnxModel(std::unique_ptr<Impl> impl);
std::unique_ptr<Impl> impl_;
};
} // namespace eve::tensor
+190
View File
@@ -0,0 +1,190 @@
#include <algorithm>
#include <cmath>
#include <sstream>
#include "tensor/OnnxInternal.h"
namespace eve::tensor::onnx_detail {
std::optional<RuntimeTensor> executeNeural(const Node& n, const std::vector<const RuntimeTensor*>& in,
OnnxCompute* compute) {
const auto& x = required(in, 0);
if (x.element != OnnxElement::Float32) return std::nullopt;
if (n.op == "InstanceNormalization") {
const auto& scale = required(in, 1);
const auto& bias = required(in, 2);
if (x.shape.size() < 3 || scale.shape != std::vector<int64_t>{x.shape[1]} || bias.shape != scale.shape ||
scale.element != x.element || bias.element != x.element)
throw Failure("InstanceNormalization shape/type mismatch");
const float epsilon = n.attrs.contains("epsilon") ? n.attrs.at("epsilon").real : 1e-5f;
if (!(epsilon > 0) || !std::isfinite(epsilon)) throw Failure("Invalid normalization epsilon");
size_t spatial = 1;
for (size_t j = 2; j < x.shape.size(); ++j) spatial *= x.shape[j];
if (!spatial) throw Failure("Empty InstanceNormalization");
if (compute) {
std::ostringstream s;
s.precision(9);
s << std::scientific;
s << "uint base=i*" << spatial << "u;precise float mean=0.0;for(uint j=0;j<" << spatial
<< "u;++j)mean+=x0[base+j];mean/=" << spatial << ".0;precise float var=0.0;for(uint j=0;j<" << spatial
<< "u;++j){float d=x0[base+j]-mean;var+=d*d;}float inv=inversesqrt(var/" << spatial << ".0+" << epsilon
<< ");for(uint j=0;j<" << spatial << "u;++j)y[base+j]=(x0[base+j]-mean)*inv*x1[i%" << x.shape[1]
<< "u]+x2[i%" << x.shape[1] << "u];";
return dispatchFloat(*compute, {&x, &scale, &bias}, x.shape, s.str(), count(x.shape) / spatial);
}
auto out = floats(x);
for (size_t i = 0; i < out.size() / spatial; ++i) {
float mean = 0, var = 0;
for (size_t j = 0; j < spatial; ++j) mean += out[i * spatial + j];
mean /= spatial;
for (size_t j = 0; j < spatial; ++j) {
float d = out[i * spatial + j] - mean;
var += d * d;
}
const float multiplier = read<float>(scale, i % x.shape[1]) / std::sqrt(var / spatial + epsilon),
offset = read<float>(bias, i % x.shape[1]);
for (size_t j = 0; j < spatial; ++j)
out[i * spatial + j] = (out[i * spatial + j] - mean) * multiplier + offset;
}
return make(x.element, x.shape, out);
}
if (n.op == "ConvTranspose") {
const auto& w = required(in, 1);
if (x.shape.size() != 3 || w.shape.size() != 3 || w.element != x.element || w.shape[0] != x.shape[1])
throw Failure("ConvTranspose requires NCL / CIO weights", DiagnosticCode::Unsupported);
const auto strides = attrs(n, "strides", {1}), pads = attrs(n, "pads", {0, 0}),
dilations = attrs(n, "dilations", {1}), extra = attrs(n, "output_padding", {0});
if (strides.size() != 1 || pads.size() != 2 || dilations.size() != 1 || extra.size() != 1)
throw Failure("ConvTranspose attribute rank mismatch");
const int64_t stride = strides[0], dilation = dilations[0], groups = attr(n, "group", 1), channels = x.shape[1],
length = x.shape[2], kernel = w.shape[2], cout = w.shape[1];
if (stride <= 0 || stride > 65536 || dilation <= 0 || dilation > 65536 || groups <= 0 || channels % groups ||
pads[0] < 0 || pads[1] < 0 || extra[0] < 0 || extra[0] >= std::max(stride, dilation) || length <= 0 ||
kernel <= 0 || cout <= 0)
throw Failure("Invalid ConvTranspose geometry");
if (n.attrs.contains("kernel_shape") && attrs(n, "kernel_shape", {}) != std::vector<int64_t>{kernel})
throw Failure("ConvTranspose kernel_shape mismatch");
const int64_t width = stride * (length - 1) + extra[0] + dilation * (kernel - 1) + 1 - pads[0] - pads[1],
outputs = cout * groups;
if (width <= 0 || width > INT32_MAX) throw Failure("Invalid ConvTranspose output extent");
std::vector<int64_t> shape{x.shape[0], outputs, width};
count(shape);
RuntimeTensor zeroBias = make(OnnxElement::Float32, {outputs}, std::vector<float>(outputs, 0));
const auto& bias = in.size() > 2 && in[2] ? *in[2] : zeroBias;
if (bias.element != x.element || bias.shape != zeroBias.shape) throw Failure("ConvTranspose bias mismatch");
if (compute) {
std::ostringstream s;
s << "uint pos=i%" << width << "u,o=i/" << width << "u%" << outputs << "u,batch=i/" << width * outputs
<< "u;precise float v=x2[o];for(uint c=0;c<" << channels / groups << "u;++c)for(uint j=0;j<" << kernel
<< "u;++j){int z=int(pos)+" << pads[0] << "-int(j)*" << dilation << ";if(z<0||z%" << stride << "!=0||z/"
<< stride << ">=" << length << ")continue;uint ch=o/" << cout << "u*" << channels / groups
<< "u+c;v+=x0[(batch*" << channels << "u+ch)*" << length << "u+uint(z/" << stride << ")]*x1[(ch*" << cout
<< "u+o%" << cout << "u)*" << kernel << "u+j];}y[i]=v;";
return dispatchFloat(*compute, {&x, &w, &bias}, shape, s.str());
}
std::vector<float> out(count(shape));
for (int64_t batch = 0; batch < x.shape[0]; ++batch)
for (int64_t o = 0; o < outputs; ++o)
for (int64_t pos = 0; pos < width; ++pos) {
float v = read<float>(bias, o);
for (int64_t c = 0; c < channels / groups; ++c)
for (int64_t j = 0; j < kernel; ++j) {
int64_t z = pos + pads[0] - j * dilation;
if (z < 0 || z % stride || z / stride >= length) continue;
int64_t ch = o / cout * (channels / groups) + c;
v += read<float>(x, (batch * channels + ch) * length + z / stride) *
read<float>(w, (ch * cout + o % cout) * kernel + j);
}
out[(batch * outputs + o) * width + pos] = v;
}
return make(x.element, shape, out);
}
if (n.op == "Resize") {
if (in.size() > 1 && in[1] && !in[1]->bytes.empty())
throw Failure("Resize ROI unsupported", DiagnosticCode::Unsupported);
std::vector<double> scales;
auto shape = x.shape;
if (in.size() > 2 && in[2] && !in[2]->bytes.empty()) {
auto s = floats(*in[2]);
scales.assign(s.begin(), s.end());
if (scales.size() != shape.size()) throw Failure("Resize scales rank mismatch");
for (size_t j = 0; j < shape.size(); ++j) {
if (!(scales[j] > 0) || !std::isfinite(scales[j]) || std::floor(shape[j] * scales[j]) > INT32_MAX)
throw Failure("Invalid Resize scale");
shape[j] = static_cast<int64_t>(std::floor(shape[j] * scales[j]));
}
} else {
shape = ints(required(in, 3));
if (shape.size() != x.shape.size()) throw Failure("Resize sizes rank mismatch");
for (size_t j = 0; j < shape.size(); ++j) {
if (x.shape[j] <= 0) throw Failure("Empty Resize input");
scales.push_back(double(shape[j]) / x.shape[j]);
}
}
count(shape);
const auto mode = n.attrs.contains("mode") ? n.attrs.at("mode").text : "nearest",
coordinate = n.attrs.contains("coordinate_transformation_mode")
? n.attrs.at("coordinate_transformation_mode").text
: "half_pixel";
for (auto d : x.shape)
if (d <= 0) throw Failure("Empty Resize input");
if (mode == "nearest") {
if (compute) {
std::ostringstream s;
s.precision(17);
s << "uint rest=i,source=0u;";
size_t stride = 1;
for (size_t j = shape.size(); j > 0; --j) {
s << "source+=uint(min(floor(float(rest%" << shape[j - 1] << "u)/" << std::scientific
<< scales[j - 1] << ")," << double(x.shape[j - 1] - 1) << "))*" << stride
<< "u;rest/=" << shape[j - 1] << "u;";
stride *= x.shape[j - 1];
}
s << "y[i]=x0[source];";
return dispatchFloat(*compute, {&x}, shape, s.str());
}
std::vector<float> out(count(shape));
for (size_t i = 0; i < out.size(); ++i) {
size_t rest = i, source = 0, stride = 1;
for (size_t j = shape.size(); j > 0; --j) {
source += std::min(int64_t(std::floor((rest % shape[j - 1]) / scales[j - 1])), x.shape[j - 1] - 1) *
stride;
rest /= shape[j - 1];
stride *= x.shape[j - 1];
}
out[i] = read<float>(x, source);
}
return make(x.element, shape, out);
}
// The speech model interpolates its final axis only. Reject other geometries explicitly.
for (size_t j = 0; j + 1 < shape.size(); ++j)
if (shape[j] != x.shape[j])
throw Failure("Linear Resize supports final axis only", DiagnosticCode::Unsupported);
if (shape.empty()) throw Failure("Resize scalar unsupported");
const auto width = shape.back(), old = x.shape.back();
if (!width) return RuntimeTensor{x.element, shape, {}};
auto position = [&](int64_t p) {
return coordinate == "half_pixel" ? (p + .5) / scales.back() - .5 : p / scales.back();
};
if (compute) {
std::ostringstream s;
s.precision(17);
s << std::scientific;
s << "uint base=i/" << width << "u*" << old
<< "u;float p=" << (coordinate == "half_pixel" ? "(float(i%" : "float(i%") << width
<< (coordinate == "half_pixel" ? "u)+0.5)/" : "u)/") << scales.back()
<< (coordinate == "half_pixel" ? "-0.5" : "") << ";float t=floor(p),f=p-t;uint a=uint(clamp(t,0.0,"
<< double(old - 1) << ")),b=uint(clamp(t+1.0,0.0," << double(old - 1)
<< "));y[i]=x0[base+a]*(1.0-f)+x0[base+b]*f;";
return dispatchFloat(*compute, {&x}, shape, s.str());
}
std::vector<float> out(count(shape));
for (size_t i = 0; i < out.size(); ++i) {
double p = position(i % width), lo = std::floor(p), f = p - lo;
size_t base = i / width * old;
out[i] = static_cast<float>(read<float>(x, base + std::clamp(int64_t(lo), int64_t(0), old - 1)) * (1 - f) +
read<float>(x, base + std::clamp(int64_t(lo + 1), int64_t(0), old - 1)) * f);
}
return make(x.element, shape, out);
}
return std::nullopt;
}
} // namespace eve::tensor::onnx_detail
+257
View File
@@ -0,0 +1,257 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <set>
#include <sstream>
#include "tensor/OnnxCompute.h"
#include "tensor/OnnxInternal.h"
namespace eve::tensor::onnx_detail {
namespace {
std::string literal(float v) {
std::ostringstream s;
s.precision(9);
s << std::scientific << v;
return s.str();
}
std::string indexCode(const std::vector<int64_t>& input, const std::vector<int64_t>& output, const char* index) {
std::string result = "0u";
size_t src = 1, dst = 1;
for (size_t j = 0; j < output.size(); ++j) {
if (j < input.size()) {
const auto dim = input[input.size() - 1 - j];
if (dim != 1)
result += "+(" + std::string(index) + "/" + std::to_string(dst) + "u%" + std::to_string(dim) + "u)*" +
std::to_string(src) + "u";
src *= dim;
}
dst *= output[output.size() - 1 - j];
}
return result;
}
} // namespace
RuntimeTensor dispatchFloat(OnnxCompute& compute, const std::vector<const RuntimeTensor*>& inputs,
const std::vector<int64_t>& shape, const std::string& body, size_t work) {
RuntimeTensor out{OnnxElement::Float32, shape, std::vector<uint8_t>(count(shape) * 4)};
if (out.bytes.empty()) return out;
if (!work) work = count(shape);
if (work > 65535u * 64u) throw Failure("GPU float dispatch limit exceeded", DiagnosticCode::Unsupported);
OnnxKernel k;
std::vector<OnnxBuffer> buffers;
k.outputBytes = out.bytes.size();
k.workItems = static_cast<uint32_t>(work);
k.source = "#version 450\nlayout(local_size_x=64)in;\n";
for (size_t i = 0; i < inputs.size(); ++i) {
if (inputs[i]->element != OnnxElement::Float32 && inputs[i]->element != OnnxElement::Int32)
throw Failure("GPU float input type mismatch");
buffers.push_back(inputs[i]->bytes.buffer());
k.source += "layout(std430,binding=" + std::to_string(i) + ")readonly buffer B" + std::to_string(i) +
(inputs[i]->element == OnnxElement::Int32 ? "{int x" : "{float x") + std::to_string(i) + "[];};\n";
}
k.source +=
"layout(std430,binding=" + std::to_string(inputs.size()) +
")writeonly buffer Y{float y[];};\nvoid main(){uint i=gl_GlobalInvocationID.x;if(i>=" + std::to_string(work) +
"u)return;" + body + "}";
auto r = compute.enqueue(k.source, buffers, k.outputBytes, k.workItems);
if (!r.ok()) throw Failure(r.error()->message(), r.error()->code());
if (r.value().size != out.bytes.size()) throw Failure("GPU output size mismatch");
out.bytes = std::move(r.value());
return out;
}
std::optional<RuntimeTensor> executeNumeric(const Node& n, const std::vector<const RuntimeTensor*>& in,
OnnxCompute* compute) {
const auto& x = required(in, 0);
if (x.element != OnnxElement::Float32) return std::nullopt;
if (n.op == "MatMul") {
const auto& b = required(in, 1);
if (b.element != x.element || x.shape.empty() || b.shape.empty()) throw Failure("MatMul type/rank mismatch");
auto as = x.shape, bs = b.shape;
const bool av = as.size() == 1, bv = bs.size() == 1;
if (av) as.insert(as.begin(), 1);
if (bv) bs.push_back(1);
const auto m = as[as.size() - 2], k = as.back(), cols = bs.back();
if (k != bs[bs.size() - 2]) throw Failure("MatMul inner dimension mismatch");
std::vector<int64_t> ab(as.begin(), as.end() - 2), bb(bs.begin(), bs.end() - 2);
auto batches = broadcast(ab, bb);
auto shape = batches;
shape.push_back(m);
shape.push_back(cols);
count(shape);
if (av) shape.erase(shape.end() - 2);
if (bv) shape.pop_back();
if (compute && m && cols) {
std::string body = "uint batch=i/" + std::to_string(m * cols) + "u,r=i/" + std::to_string(cols) + "u%" +
std::to_string(m) + "u,c=i%" + std::to_string(cols) +
"u;precise float v=0.0;for(uint j=0;j<" + std::to_string(k) + "u;++j)v+=x0[(" +
indexCode(ab, batches, "batch") + ")*" + std::to_string(m * k) + "u+r*" +
std::to_string(k) + "u+j]*x1[(" + indexCode(bb, batches, "batch") + ")*" +
std::to_string(k * cols) + "u+j*" + std::to_string(cols) + "u+c];y[i]=v;";
return dispatchFloat(*compute, {&x, &b}, shape, body);
}
std::vector<float> out(count(shape), 0);
for (size_t batch = 0; batch < count(batches); ++batch)
for (int64_t r = 0; r < m; ++r)
for (int64_t c = 0; c < cols; ++c) {
float sum = 0;
for (int64_t j = 0; j < k; ++j)
sum += read<float>(x, broadcastIndex(batch, ab, batches) * m * k + r * k + j) *
read<float>(b, broadcastIndex(batch, bb, batches) * k * cols + j * cols + c);
out[(batch * m + r) * cols + c] = sum;
}
return make(OnnxElement::Float32, shape, out);
}
static const std::set<std::string> binary{"Add", "Sub", "Mul", "Div", "Pow"};
if (binary.contains(n.op)) {
const auto& b = required(in, 1);
if (b.element != x.element) throw Failure("Float arithmetic dtype mismatch");
auto shape = broadcast(x.shape, b.shape);
if (compute) {
const std::string a = "x0[" + indexCode(x.shape, shape, "i") + "]",
v = "x1[" + indexCode(b.shape, shape, "i") + "]";
const std::string e = n.op == "Pow" ? "pow(" + a + "," + v + ")"
: a +
(n.op == "Add" ? "+"
: n.op == "Sub" ? "-"
: n.op == "Mul" ? "*"
: "/") +
v;
if (n.op == "Pow")
return dispatchFloat(*compute, {&x, &b}, shape,
"float a=" + a + ",b=" + v +
";float value;if(b==0.0)value=1.0;else "
"if(a==0.0)value=b>0.0?0.0:uintBitsToFloat(0x7f800000u);else "
"if(a<0.0)value=b==floor(b)?pow(-a,b)*(mod(abs(b),2.0)==0.0?1.0:-1.0):"
"uintBitsToFloat(0x7fc00000u);else value=pow(a,b);y[i]=value;");
return dispatchFloat(*compute, {&x, &b}, shape, "y[i]=" + e + ";");
}
std::vector<float> out(count(shape));
for (size_t i = 0; i < out.size(); ++i) {
const float a = read<float>(x, broadcastIndex(i, x.shape, shape)),
v = read<float>(b, broadcastIndex(i, b.shape, shape));
out[i] = n.op == "Add" ? a + v
: n.op == "Sub" ? a - v
: n.op == "Mul" ? a * v
: n.op == "Div" ? a / v
: std::pow(a, v);
}
return make(OnnxElement::Float32, shape, out);
}
if (n.op == "ReduceMean" || n.op == "ReduceSum") {
auto axes = n.op == "ReduceMean" ? attrs(n, "axes", {})
: in.size() > 1 && in[1] ? ints(*in[1])
: std::vector<int64_t>{};
if (axes.empty() && n.op == "ReduceSum" && attr(n, "noop_with_empty_axes", 0)) return x;
if (axes.empty())
for (size_t i = 0; i < x.shape.size(); ++i) axes.push_back(i);
std::set<int> set;
for (auto a : axes)
if (!set.insert(axis(a, x.shape.size())).second) throw Failure("Duplicate reduction axis");
std::vector<int64_t> outerShape, reducedShape, shape;
std::vector<size_t> outerStrides, reducedStrides;
size_t stride = count(x.shape);
for (size_t j = 0; j < x.shape.size(); ++j) {
if (!x.shape[j]) throw Failure("Empty reduction unsupported", DiagnosticCode::Unsupported);
stride /= x.shape[j];
if (set.contains(static_cast<int>(j))) {
reducedShape.push_back(x.shape[j]);
reducedStrides.push_back(stride);
if (attr(n, "keepdims", 1)) shape.push_back(1);
} else {
outerShape.push_back(x.shape[j]);
outerStrides.push_back(stride);
shape.push_back(x.shape[j]);
}
}
auto offset = [](size_t i, const std::vector<int64_t>& dims, const std::vector<size_t>& strides) {
size_t p = 0;
for (size_t j = dims.size(); j > 0; --j) {
p += (i % dims[j - 1]) * strides[j - 1];
i /= dims[j - 1];
}
return p;
};
auto code = [](const char* var, const std::vector<int64_t>& dims, const std::vector<size_t>& strides) {
std::string s = "0u";
size_t divisor = 1;
for (size_t j = dims.size(); j > 0; --j) {
s += "+(" + std::string(var) + "/" + std::to_string(divisor) + "u%" + std::to_string(dims[j - 1]) +
"u)*" + std::to_string(strides[j - 1]) + "u";
divisor *= dims[j - 1];
}
return s;
};
const size_t terms = count(reducedShape);
if (compute)
return dispatchFloat(*compute, {&x}, shape,
"uint base=" + code("i", outerShape, outerStrides) +
";precise float v=0.0;for(uint j=0;j<" + std::to_string(terms) +
"u;++j)v+=x0[base+(" + code("j", reducedShape, reducedStrides) + ")];y[i]=v" +
(n.op == "ReduceMean" ? "/" + std::to_string(terms) + ".0" : "") + ";");
std::vector<float> out(count(shape));
for (size_t i = 0; i < out.size(); ++i) {
float v = 0;
for (size_t j = 0; j < terms; ++j)
v += read<float>(x, offset(i, outerShape, outerStrides) + offset(j, reducedShape, reducedStrides));
out[i] = n.op == "ReduceMean" ? v / terms : v;
}
return make(OnnxElement::Float32, shape, out);
}
if (n.op == "Softmax" && compute) {
int a = axis(attr(n, "axis", -1), x.shape.size());
size_t inner = 1, outer = 1;
for (int j = 0; j < a; ++j) outer *= x.shape[j];
for (size_t j = a + 1; j < x.shape.size(); ++j) inner *= x.shape[j];
const auto channels = x.shape[a];
if (!channels || !inner) return RuntimeTensor{x.element, x.shape, {}};
auto ch = std::to_string(channels), ins = std::to_string(inner);
return dispatchFloat(*compute, {&x}, x.shape,
"uint base=i/" + ins + "u*" + ins + "u*" + ch + "u+i%" + ins +
"u;float m=-3.402823466e38;for(uint j=0;j<" + ch + "u;++j)m=max(m,x0[base+j*" + ins +
"u]);float sum=0.0;for(uint j=0;j<" + ch + "u;++j)sum+=exp(x0[base+j*" + ins +
"u]-m);for(uint j=0;j<" + ch + "u;++j)y[base+j*" + ins + "u]=exp(x0[base+j*" + ins +
"u]-m)/sum;",
outer * inner);
}
static const std::map<std::string, std::string> unary{{"Sqrt", "sqrt(v)"},
{"Exp", "exp(v)"},
{"Log", "log(v)"},
{"Sin", "sin(v)"},
{"Cos", "cos(v)"},
{"Relu", "max(v,0.0)"},
{"Sigmoid", "1.0/(1.0+exp(-v))"},
{"Tanh", "isnan(v)?v:tanh(clamp(v,-10.0,10.0))"},
{"Neg", "-v"},
{"Abs", "abs(v)"},
{"Reciprocal", "1.0/v"},
{"Floor", "floor(v)"},
{"Round", "roundEven(v)"},
{"Atan", "isinf(v)?sign(v)*1.5707963267948966:atan(v)"}};
if (n.op == "LeakyRelu" || unary.contains(n.op)) {
const float alpha = n.attrs.contains("alpha") ? n.attrs.at("alpha").real : 0.01f;
if (!std::isfinite(alpha)) throw Failure("Invalid LeakyRelu alpha");
if (compute)
return dispatchFloat(
*compute, {&x}, x.shape,
"float v=x0[i];y[i]=" + (n.op == "LeakyRelu" ? "v>=0.0?v:v*" + literal(alpha) : unary.at(n.op)) + ";");
if (n.op != "LeakyRelu" && n.op != "Reciprocal" && n.op != "Floor" && n.op != "Round" && n.op != "Atan")
return std::nullopt;
auto out = floats(x);
for (auto& v : out) {
if (n.op == "LeakyRelu")
v = v >= 0 ? v : alpha * v;
else if (n.op == "Reciprocal")
v = 1 / v;
else if (n.op == "Floor")
v = std::floor(v);
else if (n.op == "Atan")
v = std::atan(v);
else {
float lo = std::floor(v), f = v - lo;
v = lo + (f > 0.5f || (f == 0.5f && std::fmod(lo, 2.f) != 0));
}
}
return make(OnnxElement::Float32, x.shape, out);
}
return std::nullopt;
}
} // namespace eve::tensor::onnx_detail
+196
View File
@@ -0,0 +1,196 @@
#include "tensor/AffineQuant.h"
#include "tensor/AffineQuantInternal.h"
#include "tensor/OnnxGpuKernels.h"
#include "tensor/OnnxInternal.h"
#include <algorithm>
#include <cmath>
namespace eve::tensor::onnx_detail {
namespace {
affine::ByteView bytes(const RuntimeTensor& t) {
if (t.element != OnnxElement::Int8 && t.element != OnnxElement::UInt8)
throw Failure("Expected packed int8/uint8 tensor");
return {t.bytes, t.element == OnnxElement::Int8};
}
template <class T>
T checked(Result<T> result) {
if (!result.ok()) throw Failure(result.error()->message(), result.error()->code());
return std::move(result.value());
}
int zero(const std::vector<const RuntimeTensor*>& in, size_t i, const RuntimeTensor& owner) {
if (i >= in.size() || !in[i]) return 0;
if (in[i]->element != owner.element || count(in[i]->shape) != 1)
throw Failure("Expected matching scalar zero point");
return static_cast<int>(integer(*in[i]));
}
} // namespace
std::vector<RuntimeTensor> executeQuant(const Node& n, const std::vector<const RuntimeTensor*>& in,
OnnxCompute* compute) {
const auto& x = required(in, 0);
if (n.op == "DynamicQuantizeLinear") {
if (compute && count(x.shape)) {
if (x.element != OnnxElement::Float32) throw Failure("Expected FP32 quantization input");
const size_t total = count(x.shape);
auto stats = dispatchFloat(
*compute, {&x}, {3},
"float lo=0.0,hi=0.0,invalid=0.0;for(uint j=0;j<" + std::to_string(total) +
"u;++j){float v=x0[j];if(isinf(v))invalid=1.0;if(!isnan(v)){lo=min(lo,v);hi=max(hi,v);}}float "
"s=(hi==lo)?1.0:(hi-lo)/255.0;y[0]=s;y[1]=clamp(roundEven(-lo/s),0.0,255.0);y[2]=invalid;",
1);
auto values = floats(stats);
if (values[2] != 0 || !std::isfinite(values[0]) || !(values[0] > 0))
throw Failure("Nonfinite quantization input/range");
auto scale = make(OnnxElement::Float32, {}, std::vector<float>{values[0]});
auto zeroPoint = make(OnnxElement::UInt8, {}, std::vector<uint8_t>{static_cast<uint8_t>(values[1])});
auto params = make(OnnxElement::Float32, {2}, std::vector<float>{values[0], values[1]});
std::string source =
"#version 450\nlayout(local_size_x=64)in;layout(std430,binding=0)readonly buffer X{float "
"x[];};layout(std430,binding=1)readonly buffer P{float p[];};layout(std430,binding=2)writeonly buffer "
"Y{uint y[];};void main(){uint word=gl_GlobalInvocationID.x;if(word>=" +
std::to_string((total + 3) / 4) +
"u)return;uint packed=0u;for(uint c=0u;c<4u;++c){uint j=word*4u+c;if(j<" + std::to_string(total) +
"u){float v=x[j];uint "
"q=isnan(v)?0u:uint(clamp(roundEven(v/p[0])+p[1],0.0,255.0));packed|=q<<(c*8u);}}y[word]=packed;}";
const std::vector<OnnxBuffer> inputs{x.bytes.buffer(), params.bytes.buffer()};
auto q = checked(compute->enqueue(source, inputs, total, static_cast<uint32_t>((total + 3) / 4)));
return {{OnnxElement::UInt8, x.shape, std::move(q)}, std::move(scale), std::move(zeroPoint)};
}
auto input = floats(x);
// ORT CPU DynamicQuantizeLinear ignores NaN for range estimation and stores code 0.
// Exported Kokoro phase graphs produce 0/0 at zero-amplitude STFT bins.
std::vector<size_t> nanPositions;
for (size_t i = 0; i < input.size(); ++i)
if (std::isnan(input[i])) {
nanPositions.push_back(i);
input[i] = 0;
}
auto q = checked(affine::dynamicQuantize(input));
for (auto i : nanPositions) q.values[i] = 0;
return {{OnnxElement::UInt8, x.shape, std::move(q.values)},
make(OnnxElement::Float32, {}, std::vector<float>{q.scale}),
make(OnnxElement::UInt8, {}, std::vector<uint8_t>{q.zeroPoint})};
}
if (n.op == "QuantizeLinear" || n.op == "DequantizeLinear") {
const auto scales = floats(required(in, 1));
if (required(in, 1).shape.size() > 1) throw Failure("Quantization scale must be scalar or vector");
if (scales.empty()) throw Failure("Empty quantization scales");
const RuntimeTensor* zp = in.size() > 2 ? in[2] : nullptr;
const auto type = n.op == "DequantizeLinear" ? x.element : (zp ? zp->element : OnnxElement::UInt8);
if (type != OnnxElement::UInt8 && type != OnnxElement::Int8)
throw Failure("Unsupported quantization dtype", DiagnosticCode::Unsupported);
if (zp && (zp->element != type || zp->shape != required(in, 1).shape))
throw Failure("Scale/zero point mismatch");
std::vector<int32_t> zeros(scales.size(), 0);
if (zp)
for (size_t i = 0; i < zeros.size(); ++i) zeros[i] = static_cast<int32_t>(integer(*zp, i));
size_t inner = 1;
if (scales.size() > 1) {
const int a = axis(attr(n, "axis", 1), x.shape.size());
if (static_cast<size_t>(x.shape[a]) != scales.size()) throw Failure("Scale channel count mismatch");
for (size_t i = a + 1; i < x.shape.size(); ++i) inner *= x.shape[i];
}
if (n.op == "DequantizeLinear")
return {make(OnnxElement::Float32, x.shape, checked(affine::dequantize(bytes(x), scales, zeros, inner)))};
const auto values = floats(x);
std::vector<uint8_t> out(values.size());
for (size_t i = 0; i < values.size(); i += inner) {
const size_t channel = (i / inner) % scales.size();
auto q = checked(affine::quantize(std::span(values).subspan(i, inner), scales[channel], zeros[channel],
type == OnnxElement::Int8));
std::copy(q.begin(), q.end(), out.begin() + i);
}
return {{type, x.shape, std::move(out)}};
}
if (n.op == "MatMulInteger") {
const auto& b = required(in, 1);
if (x.shape.size() < 2 || b.shape.size() != 2)
throw Failure("MatMulInteger requires rank >=2 A and rank 2 B", DiagnosticCode::Unsupported);
const size_t k = x.shape.back(), columns = b.shape[1];
if (k != static_cast<size_t>(b.shape[0]) || k == 0) throw Failure("Invalid MatMulInteger inner dimension");
const size_t rows = count(x.shape) / k;
auto shape = x.shape;
shape.back() = columns;
if (compute) {
if ((x.element != OnnxElement::Int8 && x.element != OnnxElement::UInt8) ||
(b.element != OnnxElement::Int8 && b.element != OnnxElement::UInt8))
throw Failure("Expected packed integer matrices");
std::vector<int32_t> bz{zero(in, 3, b)};
return {{OnnxElement::Int32, shape,
gpuMatmulResident(*compute, x.bytes.buffer(), b.bytes.buffer(), x.element == OnnxElement::Int8,
b.element == OnnxElement::Int8, rows, k, columns, zero(in, 2, x), bz)}};
}
return {make(
OnnxElement::Int32, shape,
checked(affine::matmul(bytes(x), bytes(b), rows, k, columns, zero(in, 2, x), zero(in, 3, b), compute)))};
}
if (n.op == "ConvInteger") {
const auto& w = required(in, 1);
if ((x.shape.size() != 3 && x.shape.size() != 4) || w.shape.size() != x.shape.size())
throw Failure("ConvInteger supports 1D/2D NCHW", DiagnosticCode::Unsupported);
const size_t spatial = x.shape.size() - 2;
if (n.attrs.contains("kernel_shape") &&
n.attrs.at("kernel_shape").integers != std::vector<int64_t>(w.shape.begin() + 2, w.shape.end()))
throw Failure("Convolution kernel_shape mismatch");
auto strides = attrs(n, "strides", std::vector<int64_t>(spatial, 1));
auto pads = attrs(n, "pads", std::vector<int64_t>(2 * spatial, 0));
auto dilations = attrs(n, "dilations", std::vector<int64_t>(spatial, 1));
if (strides.size() != spatial || pads.size() != 2 * spatial || dilations.size() != spatial)
throw Failure("Invalid convolution attributes");
auto narrow = [](int64_t v) {
if (v < 0 || v > 65536) throw Failure("Convolution attribute out of range");
return static_cast<int>(v);
};
affine::ConvShape s;
s.batch = narrow(x.shape[0]);
s.channels = narrow(x.shape[1]);
s.outputs = narrow(w.shape[0]);
s.width = narrow(x.shape.back());
s.kernelW = narrow(w.shape.back());
s.strideW = narrow(strides.back());
s.dilationW = narrow(dilations.back());
s.padLeft = narrow(pads[spatial - 1]);
s.padRight = narrow(pads.back());
s.groups = narrow(attr(n, "group", 1));
if (spatial == 2) {
s.height = narrow(x.shape[2]);
s.kernelH = narrow(w.shape[2]);
s.strideH = narrow(strides[0]);
s.dilationH = narrow(dilations[0]);
s.padTop = narrow(pads[0]);
s.padBottom = narrow(pads[2]);
}
if (!s.groups || s.channels % s.groups || w.shape[1] != s.channels / s.groups)
throw Failure("Convolution channel mismatch");
std::vector<int32_t> wz{0};
if (in.size() > 3 && in[3]) {
if (in[3]->element != w.element) throw Failure("Weight zero-point type mismatch");
wz.clear();
for (auto z : ints(*in[3])) wz.push_back(static_cast<int32_t>(z));
}
if (compute) {
if ((x.element != OnnxElement::Int8 && x.element != OnnxElement::UInt8) ||
(w.element != OnnxElement::Int8 && w.element != OnnxElement::UInt8))
throw Failure("Expected packed convolution inputs");
const auto extent =
checked(affine::detail::validateConv(x.bytes.size(), w.bytes.size(), x.element == OnnxElement::Int8,
w.element == OnnxElement::Int8, s, zero(in, 2, x), wz));
std::vector<int64_t> outputShape{s.batch, s.outputs};
if (spatial == 2) outputShape.push_back(extent.height);
outputShape.push_back(extent.width);
return {{OnnxElement::Int32, outputShape,
gpuConvResident(*compute, x.bytes.buffer(), w.bytes.buffer(), x.element == OnnxElement::Int8,
w.element == OnnxElement::Int8, s, zero(in, 2, x), wz)}};
}
auto values = checked(affine::conv(bytes(x), bytes(w), s, zero(in, 2, x), wz, compute));
std::vector<int64_t> shape{s.batch, s.outputs};
if (spatial == 2)
shape.push_back(
(s.height + s.padTop + s.padBottom - int64_t(s.dilationH) * (s.kernelH - 1) - 1) / s.strideH + 1);
shape.push_back((s.width + s.padLeft + s.padRight - int64_t(s.dilationW) * (s.kernelW - 1) - 1) / s.strideW +
1);
return {make(OnnxElement::Int32, shape, values)};
}
throw Failure("Unsupported quantized operation", DiagnosticCode::Unsupported);
}
} // namespace eve::tensor::onnx_detail
+419
View File
@@ -0,0 +1,419 @@
#include "tensor/OnnxInternal.h"
#include <bit>
#include <functional>
#include <set>
namespace eve::tensor::onnx_detail {
namespace {
struct Field {
int id = 0, wire = 0;
uint64_t number = 0;
std::span<const uint8_t> bytes;
std::string text() const { return {reinterpret_cast<const char*>(bytes.data()), bytes.size()}; }
};
class Reader {
public:
explicit Reader(std::span<const uint8_t> data) : data_(data) {}
bool empty() const { return offset_ == data_.size(); }
uint64_t varint() {
uint64_t n = 0;
for (int shift = 0; shift < 70; shift += 7) {
if (offset_ == data_.size()) throw Failure("Truncated protobuf varint", DiagnosticCode::ParseError);
const uint8_t b = data_[offset_++];
if (shift == 63 && b > 1) throw Failure("Overflowing protobuf varint", DiagnosticCode::ParseError);
n |= static_cast<uint64_t>(b & 127) << shift;
if (!(b & 128)) return n;
}
throw Failure("Invalid protobuf varint", DiagnosticCode::ParseError);
}
std::span<const uint8_t> take(uint64_t n) {
if (n > data_.size() - offset_) throw Failure("Truncated protobuf field", DiagnosticCode::ParseError);
auto result = data_.subspan(offset_, static_cast<size_t>(n));
offset_ += static_cast<size_t>(n);
return result;
}
Field next() {
const uint64_t tag = varint();
if ((tag >> 3) == 0 || (tag >> 3) > 0x1fffffff)
throw Failure("Invalid protobuf tag", DiagnosticCode::ParseError);
Field f;
f.id = static_cast<int>(tag >> 3);
f.wire = tag & 7;
switch (f.wire) {
case 0: f.number = varint(); break;
case 1: f.bytes = take(8); break;
case 2: f.bytes = take(varint()); break;
case 5: f.bytes = take(4); break;
default: throw Failure("Unsupported protobuf wire type", DiagnosticCode::ParseError);
}
return f;
}
private:
std::span<const uint8_t> data_;
size_t offset_ = 0;
};
void wire(const Field& f, int expected) {
if (f.wire != expected) throw Failure("Unexpected protobuf field type", DiagnosticCode::ParseError);
}
void integers(const Field& f, std::vector<int64_t>& out) {
if (f.wire == 0)
out.push_back(std::bit_cast<int64_t>(f.number));
else {
wire(f, 2);
Reader r(f.bytes);
while (!r.empty()) out.push_back(std::bit_cast<int64_t>(r.varint()));
}
}
void reals(const Field& f, std::vector<float>& out) {
if (f.wire != 2 && f.wire != 5) throw Failure("Invalid float wire type", DiagnosticCode::ParseError);
if (f.bytes.size() % 4) throw Failure("Truncated float payload", DiagnosticCode::ParseError);
for (size_t i = 0; i < f.bytes.size(); i += 4) {
float x;
std::memcpy(&x, f.bytes.data() + i, 4);
out.push_back(x);
}
}
std::pair<std::string, RuntimeTensor> tensor(std::span<const uint8_t> data) {
Reader r(data);
RuntimeTensor v;
std::string name;
std::vector<int64_t> iv;
std::vector<float> fv;
bool raw = false, type = false;
while (!r.empty()) {
auto f = r.next();
switch (f.id) {
case 1: integers(f, v.shape); break;
case 2:
wire(f, 0);
if (f.number > 9) throw Failure("Unsupported tensor element type", DiagnosticCode::Unsupported);
v.element = static_cast<OnnxElement>(f.number);
type = true;
break;
case 4: reals(f, fv); break;
case 5:
case 7: integers(f, iv); break;
case 8:
wire(f, 2);
name = f.text();
break;
case 9:
wire(f, 2);
if (raw) throw Failure("Duplicate raw tensor data", DiagnosticCode::ParseError);
v.bytes.assign(f.bytes.begin(), f.bytes.end());
raw = true;
break;
case 14:
wire(f, 0);
if (f.number != 0) throw Failure("External tensor data unsupported", DiagnosticCode::Unsupported);
break;
case 3:
case 6:
case 10:
case 11:
case 13: throw Failure("External, segmented or unsupported tensor data", DiagnosticCode::Unsupported);
default: break;
}
}
if (!type) throw Failure("Tensor has no dtype", DiagnosticCode::ParseError);
const size_t n = count(v.shape), size = elementSize(v.element);
if (raw && (!iv.empty() || !fv.empty())) throw Failure("Ambiguous tensor payload", DiagnosticCode::ParseError);
if (!raw) {
if (v.element == OnnxElement::Float32) {
if (fv.size() != n || !iv.empty())
throw Failure("Invalid float tensor payload", DiagnosticCode::ParseError);
v.bytes.resize(n * size);
if (n) std::memcpy(v.bytes.data(), fv.data(), v.bytes.size());
} else {
if (iv.size() != n || !fv.empty())
throw Failure("Invalid integer tensor payload", DiagnosticCode::ParseError);
v.bytes.resize(n * size);
for (size_t i = 0; i < n; ++i) {
const auto x = iv[i];
if ((v.element == OnnxElement::UInt8 && (x < 0 || x > 255)) ||
(v.element == OnnxElement::Int8 && (x < -128 || x > 127)) ||
(v.element == OnnxElement::Bool && x != 0 && x != 1) ||
(v.element == OnnxElement::Int32 && (x < INT32_MIN || x > INT32_MAX)))
throw Failure("Integer initializer out of range", DiagnosticCode::ParseError);
std::memcpy(v.bytes.data() + i * size, &x, size);
}
}
}
validate(v);
return {std::move(name), std::move(v)};
}
ModelData parseGraph(std::span<const uint8_t> bytes, size_t depth);
std::pair<std::string, Attribute> attribute(std::span<const uint8_t> bytes, size_t depth) {
Reader r(bytes);
Attribute a;
std::string name;
while (!r.empty()) {
auto f = r.next();
switch (f.id) {
case 1:
wire(f, 2);
name = f.text();
break;
case 20:
wire(f, 0);
a.type = static_cast<int>(f.number);
break;
case 2: {
wire(f, 5);
std::memcpy(&a.real, f.bytes.data(), 4);
break;
}
case 3:
wire(f, 0);
a.integer = std::bit_cast<int64_t>(f.number);
break;
case 4:
wire(f, 2);
a.text = f.text();
break;
case 5:
wire(f, 2);
a.tensor = tensor(f.bytes).second;
break;
case 7: reals(f, a.reals); break;
case 8: integers(f, a.integers); break;
case 9:
wire(f, 2);
a.strings.push_back(f.text());
break;
case 6:
wire(f, 2);
a.graph = std::make_shared<ModelData>(parseGraph(f.bytes, depth + 1));
break;
case 11: throw Failure("Multiple graph attributes unsupported", DiagnosticCode::Unsupported);
case 21: throw Failure("Function attribute references unsupported", DiagnosticCode::Unsupported);
default: break;
}
}
if (name.empty() || !a.type) throw Failure("Incomplete attribute", DiagnosticCode::ParseError);
return {std::move(name), std::move(a)};
}
Node node(std::span<const uint8_t> bytes, size_t depth) {
Reader r(bytes);
Node n;
while (!r.empty()) {
auto f = r.next();
if (f.id >= 1 && f.id <= 5) wire(f, 2);
switch (f.id) {
case 1: n.inputs.push_back(f.text()); break;
case 2: n.outputs.push_back(f.text()); break;
case 3: n.name = f.text(); break;
case 4: n.op = f.text(); break;
case 5:
if (!n.attrs.insert(attribute(f.bytes, depth)).second)
throw Failure("Duplicate attribute", DiagnosticCode::ParseError);
break;
case 7:
wire(f, 2);
n.domain = f.text();
break;
case 8: throw Failure("Function overloads unsupported", DiagnosticCode::Unsupported);
default: break;
}
}
if (n.op.empty() || n.outputs.empty()) throw Failure("Incomplete node", DiagnosticCode::ParseError);
return n;
}
std::pair<std::string, Input> input(std::span<const uint8_t> bytes) {
Reader r(bytes);
std::string name;
Input value;
while (!r.empty()) {
auto f = r.next();
if (f.id == 1) {
wire(f, 2);
name = f.text();
}
if (f.id == 2) {
wire(f, 2);
Reader type(f.bytes);
while (!type.empty()) {
auto t = type.next();
if (t.id == 4) {
value.type = 0;
continue;
}
if (t.id != 1) throw Failure("Non-tensor graph input", DiagnosticCode::Unsupported);
wire(t, 2);
Reader tt(t.bytes);
while (!tt.empty()) {
auto p = tt.next();
if (p.id == 1) {
wire(p, 0);
value.type = static_cast<int>(p.number);
}
if (p.id == 2) {
wire(p, 2);
Reader shape(p.bytes);
while (!shape.empty()) {
auto d = shape.next();
if (d.id != 1) continue;
wire(d, 2);
Reader dim(d.bytes);
int64_t v = -1;
while (!dim.empty()) {
auto x = dim.next();
if (x.id == 1) {
wire(x, 0);
v = std::bit_cast<int64_t>(x.number);
}
}
value.shape.push_back(v);
}
}
}
}
}
}
if (name.empty()) throw Failure("Empty value name", DiagnosticCode::ParseError);
if (value.shape.size() > 6) throw Failure("Input rank exceeds six", DiagnosticCode::Unsupported);
for (auto d : value.shape)
if (d < -1 || d > INT32_MAX) throw Failure("Invalid interface dimension", DiagnosticCode::ParseError);
return {name, value};
}
ModelData parseGraph(std::span<const uint8_t> bytes, size_t depth) {
ModelData model;
if (depth > 16) throw Failure("ONNX graph nesting limit exceeded", DiagnosticCode::ParseError);
Reader g(bytes);
while (!g.empty()) {
auto f = g.next();
if (f.id == 1) {
wire(f, 2);
if (model.nodes.size() >= 100000) throw Failure("Node limit exceeded", DiagnosticCode::ParseError);
model.nodes.push_back(node(f.bytes, depth));
}
if (f.id == 5) {
wire(f, 2);
auto t = tensor(f.bytes);
t.second.bytes.retainAcrossRuns();
if (t.first.empty() || !model.constants.emplace(std::move(t)).second)
throw Failure("Duplicate/empty initializer", DiagnosticCode::ParseError);
}
if (f.id == 11) {
wire(f, 2);
auto v = input(f.bytes);
model.info.inputs.push_back(v.first);
if (!model.inputs.emplace(std::move(v)).second)
throw Failure("Duplicate graph input", DiagnosticCode::ParseError);
}
if (f.id == 12) {
wire(f, 2);
model.info.outputs.push_back(input(f.bytes).first);
}
if (f.id == 15) throw Failure("Sparse initializers unsupported", DiagnosticCode::Unsupported);
}
return model;
}
} // namespace
ModelData parse(std::span<const uint8_t> bytes) {
if constexpr (std::endian::native != std::endian::little)
throw Failure("ONNX requires little-endian host", DiagnosticCode::Unsupported);
if (bytes.empty() || bytes.size() > 512u * 1024u * 1024u)
throw Failure("Model size exceeds import limit", DiagnosticCode::ParseError);
ModelData model;
Reader r(bytes);
std::span<const uint8_t> graph;
while (!r.empty()) {
auto f = r.next();
if (f.id == 1) {
wire(f, 0);
model.info.irVersion = static_cast<int64_t>(f.number);
}
if (f.id == 7) {
wire(f, 2);
if (!graph.empty()) throw Failure("Duplicate graph", DiagnosticCode::ParseError);
graph = f.bytes;
}
if (f.id == 8) {
wire(f, 2);
Reader ops(f.bytes);
std::string domain;
int64_t version = 0;
while (!ops.empty()) {
auto x = ops.next();
if (x.id == 1) {
wire(x, 2);
domain = x.text();
}
if (x.id == 2) {
wire(x, 0);
version = x.number;
}
}
if (!model.opsets.emplace(domain, version).second)
throw Failure("Duplicate opset", DiagnosticCode::ParseError);
}
if (f.id == 20 || f.id == 25 || f.id == 26)
throw Failure("Training/functions/device configuration unsupported", DiagnosticCode::Unsupported);
}
if (model.info.irVersion < 3 || model.info.irVersion > 10 || !model.opsets.contains("") ||
model.opsets.at("") < 13 || model.opsets.at("") > 17)
throw Failure("Unsupported ONNX IR/opset version", DiagnosticCode::UnknownVersion);
if (model.opsets.contains("com.microsoft") && model.opsets.at("com.microsoft") != 1)
throw Failure("Unsupported Microsoft opset", DiagnosticCode::UnknownVersion);
if (graph.empty()) throw Failure("Missing ONNX graph", DiagnosticCode::ParseError);
auto parsed = parseGraph(graph, 0);
model.nodes = std::move(parsed.nodes);
model.constants = std::move(parsed.constants);
model.inputs = std::move(parsed.inputs);
model.info.inputs = std::move(parsed.info.inputs);
model.info.outputs = std::move(parsed.info.outputs);
std::set<std::string> names;
for (const auto& [name, v] : model.constants) {
names.insert(name);
model.info.initializerBytes += v.bytes.size();
}
for (const auto& [name, v] : model.inputs) names.insert(name);
for (const auto& n : model.nodes) {
if (!model.opsets.contains(n.domain)) throw Failure("Node domain has no opset", DiagnosticCode::ParseError);
for (const auto& name : n.inputs)
if (!name.empty() && !names.contains(name))
throw Failure("Input not in topological order: " + name, DiagnosticCode::ParseError);
for (const auto& name : n.outputs)
if (!name.empty() && !names.insert(name).second)
throw Failure("Duplicate graph value: " + name, DiagnosticCode::ParseError);
if (!isSupported(n)) model.info.unsupportedNodes.push_back(n.name + " [" + n.domain + "::" + n.op + "]");
}
for (const auto& out : model.info.outputs)
if (!names.contains(out)) throw Failure("Undefined graph output", DiagnosticCode::ParseError);
size_t recursiveNodes = 0;
std::function<void(const ModelData&, const std::set<std::string>&, bool)> validateNested;
validateNested = [&](const ModelData& g, const std::set<std::string>& outer, bool root) {
recursiveNodes += g.nodes.size();
if (recursiveNodes > 100000) throw Failure("Recursive node limit exceeded", DiagnosticCode::ParseError);
auto available = outer;
std::set<std::string> locals;
for (const auto& [name, t] : g.constants) locals.insert(name);
for (const auto& [name, t] : g.inputs) locals.insert(name);
for (const auto& n : g.nodes)
for (const auto& out : n.outputs)
if (!out.empty() && !locals.insert(out).second)
throw Failure("Duplicate nested graph value: " + out, DiagnosticCode::ParseError);
available.insert(locals.begin(), locals.end());
for (const auto& n : g.nodes) {
if (!model.opsets.contains(n.domain))
throw Failure("Nested node domain has no opset", DiagnosticCode::ParseError);
for (const auto& input : n.inputs)
if (!input.empty() && !available.contains(input))
throw Failure("Undefined lexical capture: " + input, DiagnosticCode::ParseError);
if (!root && !isSupported(n))
model.info.unsupportedNodes.push_back(n.name + " [" + n.domain + "::" + n.op + "]");
for (const auto& [key, a] : n.attrs)
if (a.graph) validateNested(*a.graph, available, false);
}
for (const auto& out : g.info.outputs)
if (!available.contains(out)) throw Failure("Undefined nested output: " + out, DiagnosticCode::ParseError);
};
validateNested(model, {}, true);
model.info.nodeCount = model.nodes.size();
return model;
}
} // namespace eve::tensor::onnx_detail
+360
View File
@@ -0,0 +1,360 @@
#include <algorithm>
#include <cmath>
#include <functional>
#include <queue>
#include <random>
#include <set>
#include "tensor/OnnxInternal.h"
namespace eve::tensor::onnx_detail {
namespace {
struct Value {
const RuntimeTensor* borrowed = nullptr;
std::shared_ptr<const RuntimeTensor> owned;
std::shared_ptr<std::vector<Value>> sequence;
OnnxElement sequenceElement = OnnxElement::Float32;
const RuntimeTensor& tensor() const {
if (owned) return *owned;
if (borrowed) return *borrowed;
throw Failure("Expected tensor, received sequence");
}
};
using Values = std::unordered_map<std::string, Value>;
struct Context {
OnnxCompute* compute;
uint64_t seed;
bool finite;
size_t steps = 0, liveBytes = 0;
std::unordered_map<const Node*, std::mt19937> randomStreams;
};
Value hold(RuntimeTensor t, Context& c) {
validate(t);
if (c.finite && t.element == OnnxElement::Float32)
for (size_t i = 0; i < count(t.shape); ++i)
if (!std::isfinite(read<float>(t, i))) throw Failure("Nonfinite float output");
const auto size = t.bytes.size();
if (size > 512u * 1024u * 1024u - c.liveBytes) throw Failure("ONNX live tensor memory limit exceeded");
// Context encloses every runtime Value; escaping public outputs are owning copies.
auto* raw = new RuntimeTensor(std::move(t));
c.liveBytes += size;
return {nullptr,
std::shared_ptr<const RuntimeTensor>(raw,
[&c, size](const RuntimeTensor* p) {
delete p;
c.liveBytes -= size;
}),
{}};
}
std::set<std::string> captures(const ModelData& graph);
std::set<std::string> dependencies(const Node& n) {
std::set<std::string> result;
for (const auto& i : n.inputs)
if (!i.empty()) result.insert(i);
for (const auto& [key, a] : n.attrs)
if (a.graph) {
auto names = captures(*a.graph);
result.insert(names.begin(), names.end());
}
return result;
}
std::set<std::string> captures(const ModelData& graph) {
std::set<std::string> locals, result;
for (const auto& [name, t] : graph.constants) locals.insert(name);
for (const auto& [name, t] : graph.inputs) locals.insert(name);
for (const auto& n : graph.nodes)
for (const auto& out : n.outputs)
if (!out.empty()) locals.insert(out);
for (const auto& n : graph.nodes) {
auto deps = dependencies(n);
for (const auto& d : deps)
if (!locals.contains(d)) result.insert(d);
}
for (const auto& out : graph.info.outputs)
if (!locals.contains(out)) result.insert(out);
return result;
}
void admit(const Node& n) {
if (!isSupported(n))
throw Failure(n.name + ": Unsupported ONNX node " + n.domain + "::" + n.op, DiagnosticCode::Unsupported,
n.name);
for (const auto& [key, a] : n.attrs)
if (a.graph)
for (const auto& child : a.graph->nodes) admit(child);
}
std::vector<size_t> plan(const ModelData& g, const std::vector<std::string>& requested) {
std::unordered_map<std::string, size_t> producers;
for (size_t i = 0; i < g.nodes.size(); ++i)
for (const auto& name : g.nodes[i].outputs)
if (!name.empty() && !producers.emplace(name, i).second) throw Failure("Duplicate graph output");
std::vector<bool> selected(g.nodes.size(), false);
std::vector<std::string> pending = requested;
while (!pending.empty()) {
auto name = std::move(pending.back());
pending.pop_back();
auto p = producers.find(name);
if (p == producers.end() || selected[p->second]) continue;
selected[p->second] = true;
admit(g.nodes[p->second]);
auto deps = dependencies(g.nodes[p->second]);
pending.insert(pending.end(), deps.begin(), deps.end());
}
std::vector<size_t> indegree(g.nodes.size());
std::vector<std::vector<size_t>> users(g.nodes.size());
size_t total = 0;
std::priority_queue<size_t, std::vector<size_t>, std::greater<size_t>> ready;
for (size_t i = 0; i < g.nodes.size(); ++i)
if (selected[i]) {
++total;
std::set<size_t> parents;
for (const auto& name : dependencies(g.nodes[i])) {
auto p = producers.find(name);
if (p != producers.end()) parents.insert(p->second);
}
indegree[i] = parents.size();
for (auto p : parents) users[p].push_back(i);
if (parents.empty()) ready.push(i);
}
std::vector<size_t> order;
while (!ready.empty()) {
size_t i = ready.top();
ready.pop();
order.push_back(i);
for (auto user : users[i])
if (--indegree[user] == 0) ready.push(user);
}
if (order.size() != total) throw Failure("ONNX graph has cyclic lexical dependencies", DiagnosticCode::ParseError);
return order;
}
std::vector<Value> run(const ModelData&, Values, const std::vector<std::string>&, Context&, size_t);
std::vector<Value> control(const Node& n, const std::vector<Value>& in, const Values& values, Context& c,
size_t depth) {
auto scalar = [&](size_t i) {
const auto& t = in.at(i).tensor();
if (count(t.shape) != 1) throw Failure("Control input must contain one element");
return integer(t);
};
auto seq = [&](size_t i) -> const std::vector<Value>& {
if (!in.at(i).sequence) throw Failure("Expected sequence input");
return *in[i].sequence;
};
auto sequence = [](std::vector<Value> v, OnnxElement type) {
return Value{nullptr, {}, std::make_shared<std::vector<Value>>(std::move(v)), type};
};
if (n.op == "SequenceEmpty") {
auto type = static_cast<OnnxElement>(attr(n, "dtype", 1));
elementSize(type);
return {sequence({}, type)};
}
if (n.op == "SequenceAt") {
const auto& s = seq(0);
int64_t i = scalar(1);
if (i < 0) i += static_cast<int64_t>(s.size());
if (i < 0 || static_cast<size_t>(i) >= s.size()) throw Failure("SequenceAt out of bounds");
return {s[i]};
}
if (n.op == "SequenceInsert") {
auto s = seq(0);
if (s.size() >= 100000) throw Failure("Sequence length limit exceeded");
int64_t i = in.size() > 2 ? scalar(2) : static_cast<int64_t>(s.size());
if (i < 0) i += static_cast<int64_t>(s.size());
if (i < 0 || static_cast<size_t>(i) > s.size()) throw Failure("SequenceInsert out of bounds");
const auto& t = in.at(1).tensor();
if (in[0].sequenceElement != t.element) throw Failure("Sequence element dtype mismatch");
s.insert(s.begin() + i, in[1]);
return {sequence(std::move(s), in[0].sequenceElement)};
}
if (n.op == "SplitToSequence") {
const auto& x = in.at(0).tensor();
int a = axis(attr(n, "axis", 0), x.shape.size());
std::vector<int64_t> lengths;
if (in.size() > 1 && (in[1].borrowed || in[1].owned)) {
const auto& split = in[1].tensor();
lengths = ints(split);
if (split.shape.empty()) {
if (lengths[0] <= 0) throw Failure("Invalid split size");
int64_t width = lengths[0];
lengths.clear();
for (int64_t i = 0; i < x.shape[a]; i += width) {
if (lengths.size() >= 100000) throw Failure("Sequence length limit exceeded");
lengths.push_back(std::min(width, x.shape[a] - i));
}
}
} else
lengths.assign(x.shape[a], 1);
if (lengths.size() > 100000) throw Failure("Sequence length limit exceeded");
int64_t total = 0;
for (auto size : lengths) {
if (size < 0 || size > x.shape[a] - total) throw Failure("Invalid split lengths");
total += size;
}
if (total != x.shape[a]) throw Failure("Split lengths do not cover axis");
size_t outer = 1, inner = elementSize(x.element);
for (int j = 0; j < a; ++j) outer *= x.shape[j];
for (size_t j = a + 1; j < x.shape.size(); ++j) inner *= x.shape[j];
std::vector<Value> s;
size_t offset = 0;
for (auto length : lengths) {
auto shape = x.shape;
shape[a] = length;
if (!attr(n, "keepdims", 1)) {
if (in.size() > 1 && (in[1].borrowed || in[1].owned))
throw Failure("keepdims=0 with explicit split unsupported", DiagnosticCode::Unsupported);
shape.erase(shape.begin() + a);
}
RuntimeTensor out{x.element, shape, std::vector<uint8_t>(count(shape) * elementSize(x.element))};
for (size_t o = 0; o < outer; ++o)
if (length)
std::memcpy(out.bytes.data() + o * length * inner,
x.bytes.data() + (o * x.shape[a] + offset) * inner, length * inner);
s.push_back(hold(std::move(out), c));
offset += length;
}
return {sequence(std::move(s), x.element)};
}
if (n.op == "ConcatFromSequence") {
const auto& s = seq(0);
if (s.empty()) throw Failure("Cannot concatenate empty sequence");
std::vector<RuntimeTensor> tensors;
std::vector<const RuntimeTensor*> inputs;
for (const auto& v : s) {
tensors.push_back(v.tensor());
if (attr(n, "new_axis", 0)) {
const int a = axis(attr(n, "axis", 0), tensors.back().shape.size() + 1);
tensors.back().shape.insert(tensors.back().shape.begin() + a, 1);
}
}
for (const auto& t : tensors) inputs.push_back(&t);
Node concat = n;
concat.op = "Concat";
auto out = executeShape(concat, inputs);
if (!out) throw Failure("ConcatFromSequence dispatch failed");
return {hold(std::move(*out), c)};
}
if (n.op == "If") {
if (in.at(0).tensor().element != OnnxElement::Bool) throw Failure("If condition must be boolean");
auto key = scalar(0) ? "then_branch" : "else_branch";
const auto& g = n.attrs.at(key).graph;
if (!g) throw Failure("Missing If branch");
return run(*g, values, g->info.outputs, c, depth + 1);
}
if (n.op == "Loop") {
const auto& body = n.attrs.at("body").graph;
if (!body) throw Failure("Missing Loop body");
if (in.size() < 2 || body->info.inputs.size() != in.size() || body->info.outputs.size() != in.size() - 1 ||
n.outputs.size() != in.size() - 2)
throw Failure("Loop scan outputs unsupported", DiagnosticCode::Unsupported);
const int64_t trips = (in[0].borrowed || in[0].owned) ? scalar(0) : 10000;
if (trips < 0 || trips > 10000) throw Failure("Loop trip budget exceeded");
bool condition = (in[1].borrowed || in[1].owned) ? scalar(1) != 0 : true;
std::vector<Value> state(in.begin() + 2, in.end());
for (int64_t i = 0; i < trips && condition; ++i) {
auto scope = values;
scope[body->info.inputs[0]] = hold(make(OnnxElement::Int64, {}, std::vector<int64_t>{i}), c);
scope[body->info.inputs[1]] =
hold(make(OnnxElement::Bool, {}, std::vector<uint8_t>{uint8_t(condition)}), c);
for (size_t j = 0; j < state.size(); ++j) scope[body->info.inputs[j + 2]] = state[j];
auto result = run(*body, std::move(scope), body->info.outputs, c, depth + 1);
const auto& cond = result[0].tensor();
if (cond.element != OnnxElement::Bool || count(cond.shape) != 1)
throw Failure("Loop returned invalid condition");
condition = integer(cond) != 0;
state.assign(result.begin() + 1, result.end());
}
return state;
}
if (n.op == "RandomUniformLike" || n.op == "RandomNormalLike") {
const auto& x = in.at(0).tensor();
if (attr(n, "dtype", static_cast<int>(x.element)) != 1)
throw Failure("Random output requires FP32", DiagnosticCode::Unsupported);
uint64_t seed = c.seed;
for (unsigned char ch : n.name) seed = (seed ^ ch) * 1099511628211ull;
if (n.attrs.contains("seed")) {
uint32_t bits;
std::memcpy(&bits, &n.attrs.at("seed").real, 4);
seed ^= bits;
}
auto [stream, inserted] = c.randomStreams.try_emplace(&n, static_cast<uint32_t>(seed ^ (seed >> 32)));
auto& rng = stream->second;
auto uniform = [&]() { return (double(rng()) + .5) / 4294967296.; };
std::vector<float> v(count(x.shape));
const float low = n.attrs.contains("low") ? n.attrs.at("low").real : 0,
high = n.attrs.contains("high") ? n.attrs.at("high").real : 1,
mean = n.attrs.contains("mean") ? n.attrs.at("mean").real : 0,
scale = n.attrs.contains("scale") ? n.attrs.at("scale").real : 1;
if (!std::isfinite(low) || !std::isfinite(high) || !std::isfinite(mean) || !std::isfinite(scale) ||
high < low || scale < 0)
throw Failure("Invalid random distribution parameters");
for (auto& f : v)
f = n.op == "RandomUniformLike" ? static_cast<float>(low + (high - low) * uniform())
: static_cast<float>(mean + scale * std::sqrt(-2 * std::log(uniform())) *
std::cos(6.283185307179586 * uniform()));
return {hold(make(OnnxElement::Float32, x.shape, v), c)};
}
std::vector<const RuntimeTensor*> tensors;
for (const auto& v : in) tensors.push_back(v.borrowed || v.owned ? &v.tensor() : nullptr);
auto outputs = execute(n, tensors, c.compute);
std::vector<Value> result;
for (auto& t : outputs) result.push_back(hold(std::move(t), c));
return result;
}
std::vector<Value> run(const ModelData& g, Values values, const std::vector<std::string>& requested, Context& c,
size_t depth) {
if (depth > 16) throw Failure("Execution graph nesting limit exceeded");
for (const auto& [name, t] : g.constants)
if (!g.inputs.contains(name) || !values.contains(name)) values[name] = {&t, {}, {}};
auto order = plan(g, requested);
std::unordered_map<std::string, size_t> uses;
for (auto i : order)
for (const auto& dep : dependencies(g.nodes[i])) ++uses[dep];
for (const auto& name : requested) ++uses[name];
for (auto i : order) {
const auto& n = g.nodes[i];
if (++c.steps > 1000000) throw Failure("ONNX execution step budget exceeded");
try {
std::vector<Value> in;
for (const auto& name : n.inputs) {
if (name.empty()) {
in.push_back({});
continue;
}
auto it = values.find(name);
if (it == values.end()) throw Failure("Missing feed or lexical capture: " + name);
in.push_back(it->second);
}
auto outputs = control(n, in, values, c, depth);
if (outputs.size() < n.outputs.size()) throw Failure("Operator output arity mismatch");
for (size_t j = 0; j < n.outputs.size(); ++j)
if (!n.outputs[j].empty() && uses[n.outputs[j]]) values[n.outputs[j]] = std::move(outputs[j]);
for (const auto& dep : dependencies(n))
if (--uses[dep] == 0) values.erase(dep);
} catch (const Failure& e) {
throw Failure(n.name + ": " + e.what(), e.code, e.path.empty() ? n.name : e.path);
}
}
std::vector<Value> out;
for (const auto& name : requested) {
auto it = values.find(name);
if (it == values.end()) throw Failure("Missing output: " + name, DiagnosticCode::NotFound);
out.push_back(it->second);
}
return out;
}
} // namespace
std::vector<OnnxNamedTensor> evaluate(const ModelData& model, std::span<const OnnxNamedTensor> feeds,
const std::vector<std::string>& requested, OnnxCompute* compute,
OnnxRunOptions options) {
Context c{compute, options.seed, options.requireFinite};
Values inputs;
for (const auto& f : feeds) inputs[f.name] = hold({f.tensor.element, f.tensor.shape, f.tensor.bytes}, c);
auto outputs = run(model, std::move(inputs), requested, c, 0);
std::vector<OnnxNamedTensor> result;
size_t bytes = c.liveBytes;
for (size_t i = 0; i < outputs.size(); ++i) {
const auto& t = outputs[i].tensor();
if (t.bytes.size() > 512u * 1024u * 1024u - bytes) throw Failure("ONNX output memory limit exceeded");
bytes += t.bytes.size();
result.push_back({requested[i], {t.element, t.shape, static_cast<std::vector<uint8_t>>(t.bytes)}});
}
return result;
}
} // namespace eve::tensor::onnx_detail
+196
View File
@@ -0,0 +1,196 @@
#include <algorithm>
#include <cmath>
#include <numeric>
#include <set>
#include "tensor/OnnxInternal.h"
namespace eve::tensor::onnx_detail {
std::vector<int64_t> broadcast(const std::vector<int64_t>& a, const std::vector<int64_t>& b) {
std::vector<int64_t> out(std::max(a.size(), b.size()), 1);
for (size_t i = 0; i < out.size(); ++i) {
const auto x = i < a.size() ? a[a.size() - 1 - i] : 1, y = i < b.size() ? b[b.size() - 1 - i] : 1;
if (x != y && x != 1 && y != 1) throw Failure("Broadcast dimension mismatch");
out[out.size() - 1 - i] = x == 1 ? y : x;
}
count(out);
return out;
}
size_t broadcastIndex(size_t i, const std::vector<int64_t>& shape, const std::vector<int64_t>& output) {
size_t index = 0, stride = 1;
for (size_t j = 0; j < output.size(); ++j) {
const auto coordinate = i % output[output.size() - 1 - j];
i /= output[output.size() - 1 - j];
if (j < shape.size()) {
const auto d = shape[shape.size() - 1 - j];
if (d != 1) index += coordinate * stride;
stride *= d;
}
}
return index;
}
std::optional<RuntimeTensor> executeShape(const Node& n, const std::vector<const RuntimeTensor*>& in) {
const auto& x = required(in, 0);
if (n.op == "Slice") {
auto starts = ints(required(in, 1)), ends = ints(required(in, 2));
auto axes = in.size() > 3 && in[3] ? ints(*in[3]) : std::vector<int64_t>(starts.size());
if (in.size() <= 3 || !in[3]) std::iota(axes.begin(), axes.end(), 0);
auto steps = in.size() > 4 && in[4] ? ints(*in[4]) : std::vector<int64_t>(starts.size(), 1);
if (starts.size() != ends.size() || starts.size() != axes.size() || starts.size() != steps.size())
throw Failure("Slice parameter lengths mismatch");
auto shape = x.shape;
std::vector<int64_t> begin(shape.size(), 0), step(shape.size(), 1);
std::set<int> seen;
for (size_t j = 0; j < axes.size(); ++j) {
int a = axis(axes[j], shape.size());
if (!seen.insert(a).second || !steps[j] || steps[j] == INT64_MIN) throw Failure("Invalid Slice axis/step");
const auto d = x.shape[a], st = steps[j];
auto norm = [d, st](int64_t v, bool end) {
if (v < 0) v = v < -d ? (st < 0 ? -1 : 0) : v + d;
return std::clamp(v, st < 0 && end ? int64_t(-1) : int64_t(0),
st < 0 ? std::max(int64_t(0), d - 1) : d);
};
begin[a] = norm(starts[j], false);
const auto e = norm(ends[j], true);
step[a] = st;
const int64_t distance = st > 0 ? e - begin[a] : begin[a] - e, magnitude = st > 0 ? st : -st;
shape[a] = d == 0 || distance <= 0 ? 0 : 1 + (distance - 1) / magnitude;
}
RuntimeTensor out{x.element, shape, std::vector<uint8_t>(count(shape) * elementSize(x.element))};
for (size_t i = 0; i < count(shape); ++i) {
size_t rest = i, source = 0, stride = 1;
for (size_t j = shape.size(); j > 0; --j) {
auto c = rest % shape[j - 1];
rest /= shape[j - 1];
source += (begin[j - 1] + c * step[j - 1]) * stride;
stride *= x.shape[j - 1];
}
std::memcpy(out.bytes.data() + i * elementSize(x.element), x.bytes.data() + source * elementSize(x.element),
elementSize(x.element));
}
return out;
}
if (n.op == "Concat") {
int a = axis(attr(n, "axis", 0), x.shape.size());
auto shape = x.shape;
shape[a] = 0;
for (auto* t : in) {
if (!t || t->element != x.element || t->shape.size() != shape.size())
throw Failure("Concat type/rank mismatch");
for (size_t j = 0; j < shape.size(); ++j)
if (j != a && t->shape[j] != shape[j]) throw Failure("Concat dimension mismatch");
shape[a] += t->shape[a];
}
RuntimeTensor out{x.element, shape, std::vector<uint8_t>(count(shape) * elementSize(x.element))};
size_t outer = 1, inner = elementSize(x.element);
for (int j = 0; j < a; ++j) outer *= shape[j];
for (size_t j = a + 1; j < shape.size(); ++j) inner *= shape[j];
size_t offset = 0;
for (size_t o = 0; o < outer; ++o)
for (auto* t : in) {
const size_t size = t->shape[a] * inner;
if (size) std::memcpy(out.bytes.data() + offset, t->bytes.data() + o * size, size);
offset += size;
}
return out;
}
if (n.op == "Expand") {
auto requested = ints(required(in, 1));
count(requested);
auto shape = broadcast(x.shape, requested);
RuntimeTensor out{x.element, shape, std::vector<uint8_t>(count(shape) * elementSize(x.element))};
for (size_t i = 0; i < count(shape); ++i)
std::memcpy(out.bytes.data() + i * elementSize(x.element),
x.bytes.data() + broadcastIndex(i, x.shape, shape) * elementSize(x.element),
elementSize(x.element));
return out;
}
if (n.op == "ConstantOfShape") {
auto shape = ints(x);
RuntimeTensor value = make(OnnxElement::Float32, {}, std::vector<float>{0});
if (n.attrs.contains("value")) {
if (!n.attrs.at("value").tensor) throw Failure("Missing fill value");
value = *n.attrs.at("value").tensor;
}
if (count(value.shape) != 1) throw Failure("ConstantOfShape requires one fill value");
RuntimeTensor out{value.element, shape, std::vector<uint8_t>(count(shape) * elementSize(value.element))};
for (size_t i = 0; i < out.bytes.size(); i += value.bytes.size())
std::memcpy(out.bytes.data() + i, value.bytes.data(), value.bytes.size());
return out;
}
if (n.op == "Where") {
const auto& a = required(in, 1);
const auto& b = required(in, 2);
if (x.element != OnnxElement::Bool || a.element != b.element) throw Failure("Where type mismatch");
auto shape = broadcast(broadcast(x.shape, a.shape), b.shape);
const auto size = elementSize(a.element);
RuntimeTensor out{a.element, shape, std::vector<uint8_t>(count(shape) * size)};
for (size_t i = 0; i < count(shape); ++i) {
const auto& v = x.bytes[broadcastIndex(i, x.shape, shape)] ? a : b;
std::memcpy(out.bytes.data() + i * size, v.bytes.data() + broadcastIndex(i, v.shape, shape) * size, size);
}
return out;
}
if (n.op == "Not") {
if (x.element != OnnxElement::Bool) throw Failure("Not requires boolean");
auto out = x;
for (auto& b : out.bytes) b = !b;
return out;
}
if (n.op == "Equal" || n.op == "Less" || n.op == "Greater" || n.op == "And") {
const auto& y = required(in, 1);
if (x.element != y.element || (n.op == "And" && x.element != OnnxElement::Bool))
throw Failure("Comparison type mismatch");
auto shape = broadcast(x.shape, y.shape);
RuntimeTensor out{OnnxElement::Bool, shape, std::vector<uint8_t>(count(shape))};
for (size_t i = 0; i < out.bytes.size(); ++i) {
auto a = broadcastIndex(i, x.shape, shape), b = broadcastIndex(i, y.shape, shape);
auto compare = [&](auto u, auto v) {
return n.op == "Equal" ? u == v
: n.op == "Less" ? u < v
: n.op == "Greater" ? u > v
: bool(u) && bool(v);
};
out.bytes[i] = x.element == OnnxElement::Float32 ? compare(read<float>(x, a), read<float>(y, b))
: compare(integer(x, a), integer(y, b));
}
return out;
}
if ((n.op == "Add" || n.op == "Sub" || n.op == "Mul" || n.op == "Div") && x.element != OnnxElement::Float32) {
const auto& y = required(in, 1);
if (x.element != y.element || (x.element != OnnxElement::Int64 && x.element != OnnxElement::Int32))
throw Failure("Integer arithmetic dtype mismatch");
auto shape = broadcast(x.shape, y.shape);
RuntimeTensor out{x.element, shape, std::vector<uint8_t>(count(shape) * elementSize(x.element))};
for (size_t i = 0; i < count(shape); ++i) {
int64_t a = integer(x, broadcastIndex(i, x.shape, shape)),
b = integer(y, broadcastIndex(i, y.shape, shape)), v = 0;
if (n.op == "Add") {
if ((b > 0 && a > INT64_MAX - b) || (b < 0 && a < INT64_MIN - b))
throw Failure("Integer addition overflow");
v = a + b;
}
if (n.op == "Sub") {
if ((b < 0 && a > INT64_MAX + b) || (b > 0 && a < INT64_MIN + b))
throw Failure("Integer subtraction overflow");
v = a - b;
}
if (n.op == "Mul") {
if (a && b &&
(a > 0 ? (b > 0 ? a > INT64_MAX / b : b < INT64_MIN / a)
: (b > 0 ? a < INT64_MIN / b : a < INT64_MAX / b)))
throw Failure("Integer multiplication overflow");
v = a * b;
}
if (n.op == "Div") {
if (!b || (a == INT64_MIN && b == -1)) throw Failure("Invalid integer division");
v = a / b;
}
if (x.element == OnnxElement::Int32 && (v < INT32_MIN || v > INT32_MAX))
throw Failure("Int32 arithmetic overflow");
std::memcpy(out.bytes.data() + i * elementSize(x.element), &v, elementSize(x.element));
}
return out;
}
return std::nullopt;
}
} // namespace eve::tensor::onnx_detail
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <cstdint>
#include <memory>
#include <vector>
#include "common/Result.h"
namespace eve::tensor {
/** @brief Run-scoped device storage. Readback completes pending work on the device thread.
* @note Implementations reject access after their run ends; no device handles escape runGpu. */
class OnnxDeviceStorage {
public:
/** @brief Release owning storage on the device thread. */
virtual ~OnnxDeviceStorage() = default;
/** @brief Return an owning host copy, or an explicit device/lifecycle error. */
[[nodiscard]] virtual Result<std::vector<uint8_t>> readback() const = 0;
};
/** @brief Immutable buffer shared by graph aliases; exactly one storage is authoritative.
* @note host owns CPU bytes; device owns GPU bytes. Host inputs may be cached by shared identity
* across calls only when persistent marks an immutable model initializer. Consumers use the device thread. */
struct OnnxBuffer {
std::shared_ptr<const std::vector<uint8_t>> host;
std::shared_ptr<OnnxDeviceStorage> device;
size_t size = 0;
bool persistent = false; // Immutable model initializer, eligible for bounded cross-run caching.
};
/** @brief Actual transfers and submissions made during one GPU run. */
struct OnnxTransferStats {
size_t shaderCompilations = 0, shaderCacheHits = 0;
size_t uploads = 0, downloads = 0, submissions = 0;
size_t uploadedBytes = 0, downloadedBytes = 0;
size_t bufferAllocations = 0, bufferReuses = 0;
size_t compilerPeakWorkers = 0;
};
} // namespace eve::tensor
+15
View File
@@ -250,6 +250,21 @@ elseif(EVENGINE_IOS_TEST_APP)
EVENGINE_IOS_TEST_APP=1)
else()
target_link_libraries(unit_test ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB} ${EVENGINE_WEBGPU_LIB})
if(TARGET EVTensor AND NOT EVENGINE_WEBGPU)
add_executable(onnx_engine_probe EXCLUDE_FROM_ALL "${CMAKE_SOURCE_DIR}/examples/tensor/onnx/main.cpp")
target_compile_definitions(onnx_engine_probe PRIVATE EVE_ONNX_ENGINE_PROVIDER POCO_STATIC POCO_NO_AUTOMATIC_LIBS NOMINMAX)
target_link_libraries(onnx_engine_probe eve_engine_includes ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB})
add_executable(onnx_gpu_contracts EXCLUDE_FROM_ALL main.cpp tensor_onnx_gpu.cpp)
target_compile_definitions(onnx_gpu_contracts PRIVATE POCO_STATIC POCO_NO_AUTOMATIC_LIBS NOMINMAX)
target_link_libraries(onnx_gpu_contracts eve_engine_includes ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB})
if(WIN32)
target_link_libraries(onnx_engine_probe EVBacktrace)
target_link_libraries(onnx_gpu_contracts EVBacktrace)
endif()
endif()
# Scene composition must also run in a trimmed host, where unrelated unit-test
# translation units still reference disabled optional domains.
if(TARGET EVScene_editor)
+231
View File
@@ -0,0 +1,231 @@
#include "tensor/AffineQuant.h"
#include "tensor/OnnxByteStorage.h"
#include "tensor/OnnxCompute.h"
#include "tensor/OnnxModel.h"
#include "zeroerr/unittest.h"
#include <cmath>
#include <cstring>
#include <limits>
namespace {
using Bytes = std::vector<uint8_t>;
void varint(Bytes& b, uint64_t n) {
while (n >= 128) {
b.push_back(static_cast<uint8_t>(n) | 128);
n >>= 7;
}
b.push_back(static_cast<uint8_t>(n));
}
void number(Bytes& b, int id, uint64_t n) {
varint(b, id * 8);
varint(b, n);
}
void field(Bytes& b, int id, const Bytes& value) {
varint(b, id * 8 + 2);
varint(b, value.size());
b.insert(b.end(), value.begin(), value.end());
}
Bytes text(const char* s) { return Bytes(s, s + std::char_traits<char>::length(s)); }
Bytes unknownModel() {
Bytes node, graph, output, opset, model;
field(node, 2, text("y"));
field(node, 3, text("unsupported-node"));
field(node, 4, text("UnknownOperator"));
field(graph, 1, node);
field(output, 1, text("y"));
field(graph, 12, output);
number(opset, 2, 14);
number(model, 1, 7);
field(model, 8, opset);
field(model, 7, graph);
return model;
}
} // namespace
TEST_CASE("tensor.onnx.affine.roundingAndRange") {
using namespace eve::tensor::affine;
const std::vector<float> input{-1000, -1.25f, -.75f, -.25f, .25f, .75f, 1.25f, 1000};
auto q = quantize(input, .5f, 0, true);
REQUIRE(q.ok());
CHECK(q.value() == Bytes({128, 254, 254, 0, 0, 2, 2, 127}));
auto zero = dynamicQuantize(std::vector<float>{0, 0});
REQUIRE(zero.ok());
CHECK(zero.value().scale == 1);
auto invalid = quantize(input, 0, 0, false);
CHECK(!invalid.ok());
auto nonfinite = dynamicQuantize(std::vector<float>{std::numeric_limits<float>::infinity()});
CHECK(!nonfinite.ok());
}
TEST_CASE("tensor.onnx.affine.exactAccumulators") {
using namespace eve::tensor::affine;
Bytes a(1025, 255), b(1025, 127);
auto product = matmul({a, false}, {b, true}, 1, 1025, 1);
REQUIRE(product.ok());
CHECK(product.value()[0] == 33194625);
auto mismatch = matmul({a, false}, {b, true}, 2, 1025, 1);
CHECK(!mismatch.ok());
auto zero = matmul({a, false}, {b, true}, 1, 1025, 1, 256);
CHECK(!zero.ok());
Bytes largeA(40000, 255), largeB(40000, 255);
auto overflow = matmul({largeA, false}, {largeB, false}, 1, 40000, 1);
CHECK(!overflow.ok());
}
TEST_CASE("tensor.onnx.affine.convolutionValidation") {
using namespace eve::tensor::affine;
ConvShape shape;
shape.width = 3;
shape.kernelW = 3;
shape.padLeft = 1;
shape.padRight = 1;
const Bytes x{11, 12, 13}, w{1, 1, 1};
const std::vector<int32_t> z{0};
auto result = conv({x, false}, {w, false}, shape, 10, z);
REQUIRE(result.ok());
CHECK(result.value() == std::vector<int32_t>({3, 6, 5}));
shape.groups = 2;
auto invalid = conv({x, false}, {w, false}, shape, 10, z);
CHECK(!invalid.ok());
}
TEST_CASE("tensor.onnx.import.transactionAndDiagnostics") {
using eve::tensor::OnnxModel;
auto bytes = unknownModel();
auto good = OnnxModel::load(bytes);
REQUIRE(good.ok());
CHECK(good.value()->info().unsupportedNodes.size() == 1);
auto rejected = good.value()->run({});
REQUIRE(!rejected.ok());
REQUIRE(rejected.error() != nullptr);
CHECK(rejected.error()->code() == eve::DiagnosticCode::Unsupported);
CHECK(rejected.error()->path() == "unsupported-node");
bytes.pop_back();
auto broken = OnnxModel::load(bytes);
CHECK(!broken.ok());
CHECK(good.value()->info().nodeCount == 1);
auto empty = OnnxModel::load({});
CHECK(!empty.ok());
auto overflow = OnnxModel::load(Bytes(12, 255));
CHECK(!overflow.ok());
}
TEST_CASE("tensor.onnx.import.owningOutputs") {
Bytes tensor, attribute, node, graph, output, opset, encoded;
number(tensor, 2, 1);
const float expected = 1.5f;
Bytes payload(sizeof(float));
std::memcpy(payload.data(), &expected, sizeof(float));
field(tensor, 9, payload);
field(attribute, 1, text("value"));
number(attribute, 20, 4);
field(attribute, 5, tensor);
field(node, 2, text("y"));
field(node, 4, text("Constant"));
field(node, 5, attribute);
field(graph, 1, node);
field(output, 1, text("y"));
field(graph, 12, output);
number(opset, 2, 14);
number(encoded, 1, 7);
field(encoded, 8, opset);
field(encoded, 7, graph);
auto imported = eve::tensor::OnnxModel::load(encoded);
REQUIRE(imported.ok());
encoded.clear();
auto result = imported.value()->run({});
REQUIRE(result.ok());
imported.value().reset();
REQUIRE(result.value().size() == 1);
REQUIRE(result.value()[0].tensor.bytes.size() == 4);
float actual;
std::memcpy(&actual, result.value()[0].tensor.bytes.data(), 4);
CHECK(actual == expected);
}
TEST_CASE("tensor.onnx.gpu.failureAndScope") {
class FailedGpu final : public eve::tensor::OnnxCompute {
public:
int begins = 0, ends = 0, dispatches = 0;
eve::Result<std::vector<uint8_t>> dispatch(const eve::tensor::OnnxKernel&) override {
++dispatches;
return eve::Result<std::vector<uint8_t>>::failure(
eve::Diagnostic::error(eve::DiagnosticCode::Failed, "injected GPU failure"));
}
protected:
void beginRun() noexcept override { ++begins; }
void endRun() noexcept override { ++ends; }
} gpu;
auto unsupported = eve::tensor::OnnxModel::load(unknownModel());
REQUIRE(unsupported.ok());
auto rejected = unsupported.value()->runGpu({}, gpu);
CHECK(!rejected.ok());
CHECK(gpu.dispatches == 0);
CHECK(gpu.begins == 1);
CHECK(gpu.ends == 1);
Bytes tensor, node, graph, output, opset, model;
number(tensor, 2, 1);
field(tensor, 8, text("x"));
float value = -2;
Bytes data(4);
std::memcpy(data.data(), &value, 4);
field(tensor, 9, data);
field(node, 1, text("x"));
field(node, 2, text("y"));
field(node, 3, text("relu-test"));
field(node, 4, text("Relu"));
field(graph, 1, node);
field(graph, 5, tensor);
field(output, 1, text("y"));
field(graph, 12, output);
number(opset, 2, 14);
number(model, 1, 7);
field(model, 8, opset);
field(model, 7, graph);
auto imported = eve::tensor::OnnxModel::load(model);
REQUIRE(imported.ok());
auto failed = imported.value()->runGpu({}, gpu);
REQUIRE(!failed.ok());
CHECK(failed.error()->code() == eve::DiagnosticCode::Failed);
CHECK(failed.error()->path() == "relu-test");
CHECK(gpu.dispatches == 1);
CHECK(gpu.begins == 2);
CHECK(gpu.ends == 2);
auto cpu = imported.value()->run({});
REQUIRE(cpu.ok());
float out = -1;
std::memcpy(&out, cpu.value()[0].tensor.bytes.data(), 4);
CHECK(out == 0);
}
TEST_CASE("tensor.onnx.storage.aliasAndCpuMutation") {
using eve::tensor::onnx_detail::ByteStorage;
class Deferred final : public eve::tensor::OnnxDeviceStorage {
public:
mutable int reads = 0;
eve::Result<std::vector<uint8_t>> readback() const override {
++reads;
return eve::Result<std::vector<uint8_t>>::success({1, 2, 3, 4});
}
};
auto deferred = std::make_shared<Deferred>();
ByteStorage original(eve::tensor::OnnxBuffer{{}, deferred, 4});
ByteStorage alias = original;
CHECK(alias.size() == 4);
CHECK(deferred->reads == 0);
alias.data()[0] = 9;
CHECK(deferred->reads == 1);
CHECK(static_cast<const ByteStorage&>(alias)[0] == 9);
CHECK(static_cast<const ByteStorage&>(original)[0] == 1);
CHECK(deferred->reads == 2);
ByteStorage copy = alias;
copy.data()[1] = 8;
CHECK(static_cast<const ByteStorage&>(alias)[1] == 2);
CHECK(static_cast<const ByteStorage&>(copy)[1] == 8);
}
+297
View File
@@ -0,0 +1,297 @@
#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