Skip to content

Commit fccc3df

Browse files
authored
Merge pull request #6 from asjad2401/feat/backend-dispatch
feat: GPU inference via wgpu (Metal on macOS, Vulkan elsewhere)
2 parents 62ed15f + abe91a3 commit fccc3df

19 files changed

Lines changed: 338 additions & 118 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,38 @@ jobs:
125125

126126
- name: Run ResNet-18 tests
127127
run: .venv/bin/python tests/test_resnet.py --bench
128+
129+
wgpu-tests:
130+
name: wgpu tests (software Vulkan)
131+
runs-on: ubuntu-latest
132+
needs: build
133+
steps:
134+
- uses: actions/checkout@v4
135+
136+
- name: Install Rust
137+
uses: dtolnay/rust-toolchain@stable
138+
139+
- name: Cache cargo
140+
uses: Swatinem/rust-cache@v2
141+
142+
- name: Set up Python
143+
uses: actions/setup-python@v5
144+
with:
145+
python-version: "3.12"
146+
147+
# no GPU on hosted runners: lavapipe is Mesa's CPU implementation of Vulkan,
148+
# enough to check the GPU code path end to end (not its speed)
149+
- name: Install lavapipe
150+
run: sudo apt-get update && sudo apt-get install -y mesa-vulkan-drivers
151+
152+
- name: Create virtualenv
153+
run: python -m venv .venv
154+
155+
- name: Install Python deps
156+
run: .venv/bin/pip install maturin numpy onnx onnxruntime
157+
158+
- name: Build extension
159+
run: source .venv/bin/activate && maturin develop --release
160+
161+
- name: Run op edge-case tests on wgpu
162+
run: BURN_BACKEND=wgpu .venv/bin/python tests/test_ops.py

‎CHANGELOG.md‎

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
### Performance
11-
- Static weights that onnx-ir passes as anonymous (unnamed) constants — including all
12-
conv weights — are now pre-loaded once instead of converted on every forward pass.
13-
- BatchNormalization directly after a Conv2d is folded into the conv's weight and bias
14-
at load time and removed from the graph. ResNet-18 (Model Zoo v1) at batch 8: 279 → 103 ms.
10+
### Added
11+
- GPU inference: `load_onnx(path, backend="wgpu")`, compiled to Metal on macOS and
12+
Vulkan on Linux / Windows. ResNet-18 on an Apple M-series GPU: 6.1 ms at batch 1,
13+
35.6 ms at batch 8 (~3x faster than ONNX Runtime on CPU). `available_backends()`
14+
lists the backends in the build.
15+
- CI job running the op tests on wgpu through lavapipe (software Vulkan).
16+
- ReduceMean, ReduceSum, ReduceMax, ReduceMin (needed by the torch dynamo exporter's
17+
global average pooling).
18+
- `tests/test_resnet.py`: ResNet-18 end to end vs ONNX Runtime on Model Zoo and
19+
torchvision (legacy + dynamo) exports, with optional `--bench`; run in CI.
20+
- `tests/test_ops.py`: per-op edge-case tests against ONNX Runtime, run in CI.
21+
- ONNX interpreter support for Conv2d, fused BatchNormalization, MaxPool2d,
22+
AveragePool2d, and GlobalAveragePool.
23+
- GitHub Actions CI: `cargo build`, `cargo fmt` / `cargo clippy` (non-blocking),
24+
and Python tests via `maturin develop`.
25+
- Branch protection on `main`: pull requests required, `cargo build` and
26+
`python tests (maturin)` checks required to merge.
27+
- ONNX interpreter with Gemm, Linear, Relu, Sigmoid, Tanh, Gelu, Softmax,
28+
LogSoftmax, Add, Sub, Mul, Div, Reshape, Flatten, and Transpose ops.
29+
- numpy ↔ Burn tensor bridge (`roundtrip`), one copy in each direction.
30+
- Initial maturin/pyo3 project scaffold, `burn-flex` + `onnx-ir` dependencies.
1531

1632
### Changed
33+
- All ops now run on burn-dispatch's `Dispatch` backend; the device is chosen per model.
34+
The unused `burn` umbrella dependency was replaced by `burn-dispatch`.
35+
- Backend failures that happen on a worker thread (e.g. no GPU adapter) now report the
36+
worker's panic message instead of a downstream `RecvError`, and model loading is
37+
guarded too, so they surface as `RuntimeError`.
1738
- Errors now surface as normal Python exceptions instead of `PanicException`:
1839
- `TypeError` for non-array / non-float32 inputs (with a `.astype(np.float32)` hint).
1940
- `ValueError` for wrong input count, rank, or a static dim that doesn't match the model.
@@ -22,7 +43,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2243
- Rust panic output on stderr is suppressed (the message is in the exception);
2344
set `RUST_BACKTRACE=1` to get it back.
2445

46+
### Performance
47+
- Static weights that onnx-ir passes as anonymous (unnamed) constants — including all
48+
conv weights — are now pre-loaded once instead of converted on every forward pass.
49+
- BatchNormalization directly after a Conv2d is folded into the conv's weight and bias
50+
at load time and removed from the graph. ResNet-18 (Model Zoo v1) at batch 8: 279 → 103 ms.
51+
2552
### Fixed
53+
- Broadcasting between operands of different rank (`[N, C] + [C]`, Gemm's rank-1 `C`,
54+
`[N, C, H, W] * [C, 1, 1]`) is now done numpy-style. It only worked by accident on
55+
Flex; GPU backends rejected it.
56+
- Gemm `alpha`/`beta` and BatchNorm `epsilon` use scalar ops instead of 1-element tensors.
2657
- BatchNormalization on non-4D inputs (e.g. `[N, C]` after a Linear) no longer
2758
broadcasts to a 4D output; the pre-fused fast path is only used for NCHW.
2859
- Fortran-ordered inputs (e.g. `x.T`) were read in memory order, silently scrambling data.
@@ -36,21 +67,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3667
- Unsupported ops now fail at `load_onnx` with a list of the missing ops, instead of
3768
being skipped with a warning and failing (or silently misbehaving) at inference time.
3869

39-
### Added
40-
- ReduceMean, ReduceSum, ReduceMax, ReduceMin (needed by the torch dynamo exporter's
41-
global average pooling).
42-
- `tests/test_resnet.py`: ResNet-18 end to end vs ONNX Runtime on Model Zoo and
43-
torchvision (legacy + dynamo) exports, with optional `--bench`; run in CI.
44-
- `tests/test_ops.py`: per-op edge-case tests against ONNX Runtime, run in CI.
45-
- ONNX interpreter support for Conv2d, fused BatchNormalization, MaxPool2d,
46-
AveragePool2d, and GlobalAveragePool.
47-
- GitHub Actions CI: `cargo build`, `cargo fmt` / `cargo clippy` (non-blocking),
48-
and Python tests via `maturin develop`.
49-
- Branch protection on `main`: pull requests required, `cargo build` and
50-
`python tests (maturin)` checks required to merge.
51-
- ONNX interpreter with Gemm, Linear, Relu, Sigmoid, Tanh, Gelu, Softmax,
52-
LogSoftmax, Add, Sub, Mul, Div, Reshape, Flatten, and Transpose ops.
53-
- Zero-copy numpy ↔ Burn tensor bridge (`roundtrip`).
54-
- Initial maturin/pyo3 project scaffold, `burn-flex` + `onnx-ir` dependencies.
55-
5670
[Unreleased]: https://github.com/asjad2401/burn-python/commits/main

‎Cargo.toml‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,26 @@ crate-type = ["cdylib"]
1010
[dependencies]
1111
pyo3 = { version = "0.28", features = ["extension-module"] }
1212
numpy = "0.28"
13-
burn = { version = "0.21", features = ["flex"] }
1413
burn-flex = "0.21"
14+
# listed directly so its default features (autotune, fusion) are on
15+
burn-wgpu = "0.21"
1516
burn-backend = "0.21"
1617
onnx-ir = "0.21"
1718
cubecl-common = "0.10"
1819
bytes = "1"
1920
bytemuck = { version = "1", features = ["extern_crate_alloc"] }
2021

22+
# All ops go through burn-dispatch: Flex on CPU, wgpu on GPU. The GPU path is compiled
23+
# to each platform's native shading language — Metal (MSL) on macOS, Vulkan (SPIR-V)
24+
# elsewhere — which is several times faster than the portable WGSL path (no access to
25+
# matrix hardware). burn-dispatch allows only one wgpu flavor per build, hence per target.
26+
# wgpu loads the GPU driver at runtime, so CPU-only machines still work with "flex".
27+
[target.'cfg(target_os = "macos")'.dependencies]
28+
burn-dispatch = { version = "0.21", default-features = false, features = ["std", "flex", "metal"] }
29+
30+
[target.'cfg(not(target_os = "macos"))'.dependencies]
31+
burn-dispatch = { version = "0.21", default-features = false, features = ["std", "flex", "vulkan"] }
32+
2133
[profile.release]
2234
lto = true
2335
codegen-units = 1

‎README.md‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ x = np.random.randn(1, 3, 224, 224).astype(np.float32)
1515
output = model([x])[0]
1616
```
1717

18+
## Backends
19+
20+
```python
21+
burn.available_backends() # ['flex', 'wgpu']
22+
model = burn.load_onnx("model.onnx", backend="wgpu") # run on the GPU
23+
```
24+
25+
| backend | runs on | notes |
26+
|---------|---------|-------|
27+
| `"flex"` (default) | CPU | pure-Rust [burn-flex](https://github.com/tracel-ai/burn) backend |
28+
| `"wgpu"` | GPU | Metal on macOS, Vulkan on Linux / Windows; first call per input shape compiles and autotunes kernels |
29+
30+
Both use Burn's dispatch backend, so the same interpreter runs on either. If no usable
31+
GPU is found, `load_onnx(..., backend="wgpu")` raises a `RuntimeError` naming the problem.
32+
1833
## Status
1934

2035
Early development. The numpy ↔ Burn tensor bridge is done, and the ONNX interpreter
@@ -33,15 +48,16 @@ ResNet-18 runs end to end and matches ONNX Runtime (max logit diff < 1e-5, same
3348
for the ONNX Model Zoo exports (`resnet18-v1-7`, `resnet18-v2-7`) and torchvision's
3449
`resnet18` exported with both `torch.onnx` exporters (legacy and dynamo).
3550

36-
CPU latency vs ONNX Runtime (torchvision export, burn `flex` backend, median of 10 runs).
37-
Performance varies a lot by platform:
51+
Latency vs ONNX Runtime on CPU (torchvision export, median of 15 runs after warm-up).
52+
Numbers vary a lot by platform:
3853

39-
| platform | batch | burn-python | ONNX Runtime | burn / ORT |
40-
|----------|------:|------------:|-------------:|-----------:|
41-
| Apple M-series (10 cores) | 1 | 22.6 ms | 14.7 ms | 1.5× slower |
42-
| Apple M-series (10 cores) | 8 | 105.0 ms | 118.5 ms | 0.9× (faster) |
43-
| Linux x86 (GitHub Actions runner) | 1 | 58.0 ms | 18.7 ms | 3.1× slower |
44-
| Linux x86 (GitHub Actions runner) | 8 | 335.7 ms | 145.9 ms | 2.3× slower |
54+
| platform | backend | batch 1 | batch 8 |
55+
|----------|---------|--------:|--------:|
56+
| Apple M-series (10 cores) | burn-python `wgpu` (Metal GPU) | **6.1 ms** | **35.6 ms** |
57+
| Apple M-series (10 cores) | burn-python `flex` (CPU) | 22.4 ms | 135.7 ms |
58+
| Apple M-series (10 cores) | ONNX Runtime (CPU) | 14.9 ms | 123.0 ms |
59+
| Linux x86 (GitHub Actions runner) | burn-python `flex` (CPU) | 58.0 ms | 335.7 ms |
60+
| Linux x86 (GitHub Actions runner) | ONNX Runtime (CPU) | 18.7 ms | 145.9 ms |
4561

4662
Run `python tests/test_resnet.py --bench` to measure on your machine.
4763

@@ -62,6 +78,8 @@ python tests/test_bridge.py # numpy <-> Burn tensor bridge
6278
python tests/compare_ort.py # correctness + perf vs ONNX Runtime
6379
python tests/test_ops.py # op edge cases (padding, pooling, reshape) vs ORT
6480
python tests/test_resnet.py # ResNet-18 end to end vs ORT (add --bench for timings)
81+
82+
BURN_BACKEND=wgpu python tests/test_ops.py # same tests on the GPU backend
6583
```
6684

6785
## License

‎burn_python/__init__.py‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import numpy as np
2-
from ._burn_python import __version__, OnnxModel
2+
from ._burn_python import __version__, OnnxModel, available_backends
33
from . import _burn_python as _ext
44

55
def roundtrip(arr: np.ndarray) -> np.ndarray:
@@ -9,9 +9,11 @@ def roundtrip(arr: np.ndarray) -> np.ndarray:
99
return _ext.roundtrip(arr)
1010

1111
def load_onnx(path: str, backend: str = "flex") -> OnnxModel:
12-
"""Load an ONNX model for inference."""
13-
if backend != "flex":
14-
raise ValueError(f"unsupported backend '{backend}' — only 'flex' is available right now")
15-
return _ext.load_onnx(path)
12+
"""Load an ONNX model for inference.
1613
17-
__all__ = ["__version__", "load_onnx", "roundtrip", "OnnxModel"]
14+
backend: "flex" (CPU, default) or "wgpu" (GPU via Metal / Vulkan / DX12).
15+
See available_backends() for what this build supports.
16+
"""
17+
return _ext.load_onnx(str(path), backend)
18+
19+
__all__ = ["__version__", "load_onnx", "available_backends", "roundtrip", "OnnxModel"]

‎src/interpreter/context.rs‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::collections::HashMap;
66
use burn_backend::backend::ops::FloatTensorOps;
77
use onnx_ir::ir::{Argument, ValueSource};
88

9-
use crate::tensor::{B, FloatPrim, default_device};
9+
use crate::tensor::{B, Device, FloatPrim};
1010

1111
/// Key a static/constant argument is pre-loaded under in the weights map.
1212
/// onnx-ir often hands initializers over as anonymous statics (name ""), so those
@@ -25,13 +25,15 @@ pub type OpResult = Result<(), String>;
2525
pub struct ExecutionContext<'w> {
2626
tensors: HashMap<String, FloatPrim>,
2727
weights: &'w HashMap<String, FloatPrim>,
28+
device: &'w Device,
2829
}
2930

3031
impl<'w> ExecutionContext<'w> {
31-
pub fn new(weights: &'w HashMap<String, FloatPrim>) -> Self {
32+
pub fn new(weights: &'w HashMap<String, FloatPrim>, device: &'w Device) -> Self {
3233
Self {
3334
tensors: HashMap::new(),
3435
weights,
36+
device,
3537
}
3638
}
3739

@@ -56,10 +58,7 @@ impl<'w> ExecutionContext<'w> {
5658
ValueSource::Static(_) => weight_key(arg)
5759
.and_then(|key| self.weights.get(&key).cloned())
5860
// not pre-loaded (e.g. non-float data) — convert on the fly
59-
.or_else(|| {
60-
arg.value()
61-
.map(|d| B::float_from_data(d, &default_device()))
62-
}),
61+
.or_else(|| arg.value().map(|d| B::float_from_data(d, self.device))),
6362
ValueSource::Optional => None,
6463
}
6564
}

0 commit comments

Comments
 (0)