Backends to merge - #1234
Conversation
Add a new USE_BACKEND=ONNX option that runs inference through ONNX Runtime, reusing the existing OnnxModelBuilder graph emitter (shared with the TensorRT backend) so the IO protocol and output decode are identical to TensorRT. Only onnxbackend.cpp is new; the rest is wiring (CMake, setup, version info, config keys, example config). The backend supports any ONNX Runtime execution provider. It is primarily useful for running KataGo on non-NVIDIA accelerators that have an EP, e.g. Intel GPUs/NPUs via the OpenVINO EP. Because the official prebuilt ONNX Runtime packages do not ship those EPs, the Compiling.md section documents building ONNX Runtime from source with the desired provider enabled. onnxmodelbuilder.cpp: declare graph inputs in consumption order (InputSpatial, InputGlobal, InputMask) rather than the previous InputMask-first order. This is purely cosmetic for backends that bind inputs by name (TensorRT), but is required for the OpenVINO EP, which builds its name->index map from declaration order while the ORT runtime feeds the EP kernel inputs in consumption order -- with InputMask first the EP misroutes the mask tensor into the InputSpatial port.
onnxmodelbuilder: use Pow(x,2.0) instead of Mul(x,x) for RMSNorm square OpenVINO RMSFusion (rms_fusion.cpp:38) matches Power(x, const(2)) but not Mul(x,x). Without this, all 66 RMSNorm nodes in the b11c768 transformer run as unfused ReduceMean->Sqrt->Div chains. Benchmark (Arc B580, NHWC, numStreams=2, 10 threads, 800 visits): Before: 347.49 visits/s 296.04 nnEvals/s After: 416.52 visits/s 353.14 nnEvals/s (+19.9%) Also add KATAGO_DUMP_ONNX env-var debug aid to dump the serialized ONNX model before session creation. Co-Authored-By: Claude <noreply@anthropic.com> @
…LE8 subgraphs hurting NPU performance
Add per-thread OpenVINO device type and batch size config for multi-device inference - onnxOpenVINODeviceTypeThread<N>: assign different device types (CPU/GPU/NPU) to individual server threads, enabling simultaneous heterogeneous inference - onnxOpenVINODeviceConfig_<DEV>_<Option>: per-device-type EP option overrides (num_streams, precision, etc.) for fine-grained device tuning - nnMaxBatchSizeThread<N>: per-thread max batch size to prevent fast devices from being starved by slow devices sharing the same eval queue - extractShortDeviceName(): maps GPU.0/GPU.1/AUTO:MULTI:HETERO: strings to short device names for config lookup - Enhanced printDevices() to document the new multi-device config options Co-Authored-By: Claude <noreply@anthropic.com> @
Per review feedback, drop the opt-in boolean: the ONNX graph's inputs are always declared in the fixed order InputSpatial, InputGlobal, InputMask, which the OpenVINO EP under ONNX Runtime requires. The claim that this is a no-op for name-bound backends (TRT/CUDA/CoreML) was not verified, so it is removed from the comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The nnMaxBatchSizeThread<N> feature (introduced in 815378d) modified the shared NNEvaluator core (nneval.cpp/h, setup.cpp) and was unrelated to the ONNX backend itself, widening the review surface of this PR. Remove it to keep the PR focused on the ONNX backend; a future dedicated PR can revisit per-thread batch sizing, addressing the warmup-consistency and setNumThreads reset semantics concerns raised in review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Fail loudly when the global useFP16 flag is requested (true): inference precision is controlled by the ONNX Runtime execution provider, so a true request cannot be honored and must not be silently ignored - Link protobuf via protobuf::libprotobuf for portability, defaulting Protobuf_USE_STATIC_LIBS to TRUE since a from-source ORT build bundles a static libprotobuf (avoids PROTOBUF_USE_DLLS breaking the Windows link) - Default onnxTransformerNHWC to true, matching the TensorRT backend's trtTransformerNHWC default; only affects transformer trunks, which convnets ignore - Document the useFP16 behavior and the new transformerNHWC default in gtp_example.cfg Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The OpenVINO execution provider runs inference in FP16 by default (the graph is emitted fp32, but the EP downcasts internally), so the FP16 dynamic-range workaround applies here. The previous skip left large-board convnet activations within ~2.2x of the FP16 overflow limit. Measured on M2 (28-block, 512-channel pure convnet) at 50x50, with and without scale8: - Main-path activation peak: 29284 -> 3660. Without scale8 that is 44.7% of the FP16 max (65504), i.e. only 2.24x headroom, and value_head.linear2 is the hotspot; with scale8 it is 5.6% of FP16 max, 17.9x headroom. - Real FP16 GPU inference: on value-range-20 random inputs the no-scale8 graph emits NaN across every output (5000 NaN elements in OutputPolicy); the scale8 graph stays finite and remains an exact 8x rescale of outputs. - Overflow-threshold scan: no-scale8 starts overflowing at ~15x input value range, i.e. only extreme positions, matching the real-world reports of b28 convnets overflowing and crashing on large boards, which is exactly what scale8 was introduced to prevent. Trade-off: MISH_SCALE8 subgraphs block OpenVINO's fused-Mish optimization (~2x slower FP16 inference on large-board convnets). Add onnxSkipScale8 (default false) to opt out for FP32 precision or small-board/transformer workloads where FP16 overflow is not a practical risk. Distributed selfplay (contribute) always forces onnxSkipScale8 to false: FP16-overflow NaN rows must never be uploaded to the shared training set. Match the TensorRT, CUDA, and OpenCL backends, which apply this unconditionally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ONNX Runtime intra-op thread pool was pinned to a single thread unconditionally, for every execution provider. That significantly hurts the CPU provider, which relies on ORT's default multi-threaded intra-op execution. Only the OpenVINO provider wants the pin: the EP runs the graph nodes itself and manages its own inference threads via the num_of_threads provider option, so ORT's intra-op pool is left with only the few EP-external nodes. With one ORT session per nn-server thread, leaving the default intra-op thread count would oversubscribe the CPU with N x M worker pools. Other providers now use the ORT default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…device_type The OpenVINO EP's device_id provider option is deprecated in ONNX Runtime and only accepts the bare device names CPU/GPU/NPU - any other value throws at session creation. The backend was passing a numeric per-thread device index to it, so a multi-GPU setup (deviceIdxForThread > 0 via the gpuToUse*/deviceToUse* config keys) would fail, as would the documented onnxOpenVINODeviceId = 0 example. Now the per-thread device index is instead appended to device_type as an OpenVINO device suffix (GPU -> GPU.1), which is how the OpenVINO EP selects among multiple devices. The onnxOpenVINODeviceId config key and the device_id provider option are removed, and the config docs updated. Note: this was verified against the ONNX Runtime source (OpenVINO EP provider-option parsing) and with the standard build/tests only - I have no dual-GPU hardware here, so the multi-device path has not been exercised on real hardware. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds .github/workflows/onnx-build.yml, which builds the ONNX backend (USE_BACKEND=ONNX) on Windows against a from-source ONNX Runtime carrying the OpenVINO execution provider (official prebuilt ORT packages do not ship the OpenVINO EP), runs `katago runtests`, and uploads a self-contained runnable directory as an artifact for easy download. Design notes: - Triggered only by push to feature/onnx-backend and by workflow_dispatch, so it does not run on master after the PR merges and never burns upstream CI minutes on the ~1h ORT build; maintainers may edit or delete the file freely. config/ and doc-only changes are filtered out via paths-ignore. - Runtime dependencies (ORT install tree + protobuf + zlib) are cached under deps/install; only the first run compiles ONNX Runtime. - Build recipes mirror the locally-verified scripts (build_ort.bat / build_katago_onnx.bat): OpenVINO 2026.2.1 toolkit download, ORT build.py with --use_openvino GPU, protobuf staged from the ORT build tree, and a static zlib. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first CI run failed at "Checkout ONNX Runtime source": the workflow pinned onnxruntime to the ref v1.29.0, but that release tag does not exist yet (VERSION_NUMBER is 1.29.0 while upstream is still pre-release). Pin instead to the exact commit the ONNX backend was verified against locally (7e76a52398, verified to exist on upstream master via the GitHub API), so the CI source matches the locally-tested snapshot byte-for-byte rather than drifting with master. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings in upstream's three new commits: 978acec (serialize TRT engine builds), 5ae42f8 (HumanSL support in the onnx/trt path), 2292f25 (TRT automatic workspace sizing). Conflict resolution in onnxmodelbuilder.cpp: keep this branch's fixed input declaration order (InputSpatial, InputGlobal, InputMask) required by the OpenVINO EP, and slot HumanSL's InputMeta declaration between InputGlobal and InputMask (InputMeta is an NC11 vector input consumed at the trunk's initial bias, alongside InputGlobal). Position is empirical and pending verification against an actual HumanSL model. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified after merging upstream's HumanSL support: b18c384nbt-humanv0 (model v15, metaEncoderVersion > 0) runs end-to-end on the OpenVINO EP with inputs declared as InputSpatial, InputGlobal, InputMeta, InputMask (113 visits/s, no NaN). Record the verified order in the comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The second CI run built ORT and staged protobuf (both took ~1h) then failed
at "Build zlib (static)": cmake --install resolved the install prefix to the
literal string "D:/a/KataGo/KataGo/$env:GITHUB_WORKSPACE/deps/install/zlib"
- unquoted $env:GITHUB_WORKSPACE was not expanded by pwsh here (quoted
"$env:RUNNER_TEMP" and cmd's %CD% both worked). The same broken pattern was
also present in the "Configure KataGo" step, which never got to run.
Replace every $env:GITHUB_WORKSPACE in CMake -D values with the GitHub
expression ${{ github.workspace }}, which the runner expands before handing
the line to the shell, independent of the shell's variable semantics.
Also extend on.push.branches to include ci/onnx-windows so this fix can be
validated on its own branch without touching the PR branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After validating the fix on ci/onnx-windows, scope on.push.branches back to feature/onnx-backend so the workflow does not fire on the temporary branch once it is merged back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on ignored device index - LoadedModel.scale8Resolved: plain bool under the existing mutex instead of std::atomic. All accesses were already under scale8Mutex, so the atomic added no synchronization; the mutex is what establishes the happens-before between the scale8 modelDesc write and each thread's subsequent graph build. Remove the now-unused <atomic> include. - ComputeContext.requireExactNNLenStored was a dead field (declared and initialized, never read); requireExactNNLen is already passed per-handle. Delete it. - OpenVINO device_type: when a nonzero device index is configured but the device_type is already a composite/qualified string (AUTO:/MULTI:/HETERO:), the index was silently ignored. Log a warning pointing at the per-thread onnxOpenVINODeviceTypeThread<N> override, and document the interaction in gtp_example.cfg. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…whitelist to a table Only the OpenVINO EP is verified upstream; the other onnxProvider options are experimental code paths. Make that explicit everywhere a provider is listed, with Compiling.md as the single source of truth: - Compiling.md: add an "Execution provider support matrix" (Verified / Experimental / Needs work) with platform, ORT build flag, and runtime deps per provider; reword the intro to say the backend "selects" rather than "supports" several providers. - gtp_example.cfg: annotate onnxProvider options with the same verification status and point at the matrix. - onnxbackend.cpp: hoist the hard-coded provider whitelist into a kKnownProviders table (shared by validation and the unknown-provider error, which now cites the matrix), note in the file header and printDevices that only OpenVINO is verified upstream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- onnx-cpu-build.yml: always-on regression job (pull_request + push to master + workflow_dispatch) that downloads the official prebuilt ONNX Runtime CPU package (no from-source ORT build: ~5 min vs ~1-3h for the OpenVINO job), builds static protobuf 3.21 (/MD) and zlib only on cache miss, builds with USE_BACKEND=ONNX, runs runtests, and verifies the backend via `katago version`. Complements the branch-scoped OpenVINO job with cheap always-on coverage. - CMakeLists.txt: ONNX include detection falls back to the flat include/ layout of official prebuilt ORT packages, alongside the include/onnxruntime/ layout of a from-source install. Note: ci/onnx-windows is temporarily in the push trigger so the workflow can be exercised before it exists on the default branch; drop it before merging to master. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Include dml_provider_factory.h on Windows (headers come from the
Microsoft.ML.OnnxRuntime.DirectML NuGet package), guarded by __has_include:
the stock CPU prebuilt does not ship this header, so builds against it still
compile and the DirectML provider fails at runtime with a clear message.
- Add "directml" to the provider whitelist and to the error message listing.
- In ComputeHandle, enable the DirectML EP via OrtDmlApi obtained from
GetExecutionProviderApi("DML", ...): the plain export in dml_provider_factory.h
is deprecated. Per the ORT DirectML docs the EP requires DisableMemPattern and
sequential execution mode, both set here. device_id maps from the per-thread
GPU index.
- DirectML EP is experimental (unverified upstream); on non-Windows platforms the
provider fails loudly, matching the CoreML pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…actions Replace onnx-build.yml (OpenVINO, from-source ORT) and onnx-cpu-build.yml (CPU, prebuilt ORT) with a single onnx-backend.yml that drives a matrix of execution providers. The only real difference between providers - how ONNX Runtime is obtained - is encapsulated in .github/actions/onnx-prepare-ort (prebuilt zip / DirectML NuGet / from-source build), and the shared configure-build-test-stage pipeline lives in .github/actions/onnx-build-katago. - Matrix rows today: cpu + directml (fast, prebuilt/nuget, minutes) under build-fast, openvino (from-source, 1-3h) under build-slow. - Trigger policy by tier: fast jobs run on PR + master push + dispatch so the ONNX backend keeps a cheap always-on regression guard; slow from-source jobs run only on workflow_dispatch so they never burn upstream CI minutes. - Adding a backend = one matrix row + a fetch/build recipe in onnx-prepare-ort. - GitHub-hosted runners have no GPU, so from-source jobs verify build + EP wiring only; real GPU inference must be validated on a GPU machine. - ci/onnx-windows stays in push.branches as a temporary trigger (dispatch needs the file on the default branch); remove it before merging to master. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- build-fast: + ubuntu-latest CPU (prebuilt ORT tgz) - onnx-prepare-ort: prebuilt mode now fetches the Linux tgz (zip is Windows-only) - onnx-build-katago: Linux staging rewrites DT_RUNPATH to $ORIGIN via patchelf so the downloaded binary finds its sibling libonnxruntime.so*, matching the Windows DLL layout Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- build-tensorrt: dispatch-only job in nvcr.io/nvidia/tensorrt:25.03-py3 (CUDA 12.8 + TensorRT 10.9, the combo ORT is tested against). Bootstrap installs git/node20/ninja, cmake >= 3.28 (NGC ships 3.27), and git safe.directory for the docker-mounted workspace; then onnx-prepare-ort builds ORT with --use_tensorrt --use_cuda, limited to sm_89 (RTX 4090) instead of ORT's 10-arch default. - onnx-build-katago: use $GITHUB_WORKSPACE (resolves in container jobs where github.workspace points at the runner path), and don't assume sudo for patchelf. - cache zlib/protobuf + ORT explicitly (actions/cache@v4's post-save is unreliable in composite actions), keyed identically to the restore steps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Looks good to me. I pulled the PR on Windows 11 and successfully built and verified the CPU(ONNX), DirectML, and OpenVINO targets. To test correctness, I used a slightly modified runnonmanyposestest (adjusted so it could measure ONNX backends as well as human/metaEncoder models) across 254 positions. The test covered b11, humanv0, b18, b28, and b40. The runnonmanyposestest results look good:
I also verified ownership correctness using a custom script calling the I haven't tested Linux builds yet. Thanks for working on these refactors! |
|
@lightvector Thanks for our working! I just do a upgrade for v1.17.2. Just including a very little change: 86c7aaa And I am sure everything is OK to merge! My test result & report as following: Tested the tip of #1234 (66b12ac) on my machine. ROCm backend builds and runs. Environment: RX 7900 XTX (gfx1100), ROCm 7.14, MIOpen 3.5.2, Composable Kernel 1.2.0, clang 23, Linux x86-64. Build: Clean configure + full build ( Functionality: All four models load and play via GTP (genmove returns sane moves): b10c384h6nbttflrs, b10c512h8nbt3tflrs-fson-silu-rsnh, b11c768h12nbt3tflrs-fson-silu, kata1-zhizi-b40c768nbt-s11272M-d5935M. Benchmark (peak visits/s, back-to-back on the same GPU, vs my AMD_GPU branch at v1.17.2 which is what dc7dd78 was squashed from):
The first three are faster than before, which is great. However, the largest transformer (b11c768h12, also the flagship model of the three new ones) shows a reproducible ~15% regression (reproduced across two separate runs, so not thermal drift). Some isolation I did on the b11 regression:
But even with this performance regression, overall it looks good to me and I think it's worth merging. |
onnx-prepare-ort fetches the Microsoft.AI.DirectML redistributable (which provides DirectML.dll, a dependency of the ORT DirectML package) into deps/directml, and onnx-build-katago stages it into release/. Without it the artifact lacks DirectML.dll and the DirectML provider cannot initialize on Windows 10, whose inbox DirectML is stuck at 1.1.0 (feature level 2.0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Should fix #1222 (comment) DirectML issues. |
|
Can you provide a compiled file for me to test? I have tested #1222 before. The NPU backend have some bugs, such as failing to load the b40 networks, missing some cfg files. And, I have doubts about why setting the NPU_nnMaxBatchSizeThread more than 1 in |
|
Thanks for the detailed feedback. I cannot reproduce the b40 load failure you hit, b11, b18, b28, b40 works on both GPU and NPU on this branch. commits: e01e4a1 — KataGo's search varies its NN batch size with the number of concurrent evaluations, and the OpenVINO EP compiled device code per batch size on first use, stalling the first searches by seconds. Each server thread now runs a throwaway forward at startup for every batch size it will use (NPU: 1; other devices: up to 4). On a Core Ultra 285H, 2-thread GPU throughput went from 16.9 to ~122 visits/s with no stalls. ae7e731 — A batch of 2 on the NPU triggered a whole-graph recompile of 25-45 s on its first occurrence, since the EP forces static shapes per batch size there. NPU compute handle now split any batch above 1 into per-row batch-1 forwards, which costs nothing on this single-stream device and keeps the compiled graph stable. The transformer model at 2 threads went from 1.87 to 4.35 visits/s with no stalls (mixed GPU+NPU at 4 threads: 9.3 to 45.2). c025a30 — The OpenVINO section had accumulated options with little value for a single-session synchronous backend, plus a per-device override layer that made the config hard to follow. onnxOpenVINONumOfThreads, onnxOpenVINOModelPriority, and the entire onnxOpenVINODeviceConfig_* layer are removed, and the bare CPU device is rejected in favor of onnxProvider = cpu. The OpenVINO config is now seven flat keys, with onnxSkipScale8 = true documented as a ~2x (GPU) / ~4x (NPU) lever. Latest builds and config files for gpu, npu and both: https://github.com/seniorfish/KataGo/releases/tag/v1.17.2-onnx-openvino |
Drop onnxOpenVINONumOfThreads, onnxOpenVINOModelPriority and the onnxOpenVINODeviceConfig_<Device>_<Option> override layer - none of them have value for KataGo's per-session synchronous inference. Reject the bare CPU device_type: OpenVINO is for GPU/NPU acceleration only, use onnxProvider = cpu (or the Eigen backend) for CPU inference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This PR is now merged. Thanks for all the help and testing and contributions everyone! Please submit any followup PRs or things I might have missed in the discussion as as PRs against the new tip. @seniorfish I included most of your followup commits but I omitted the two involving warmup for the moment, and NPU splitting (which I think was codewise entangled with / conflicting if warmup was omitted) . Can you see to what degree this is possible using the same warmup mechanism as was implemented already for the CUDA backend, rather than writing a new warmup mechanism locally? For example maybe adding a |
|
On the NPU, for b11c768h12nbt3, On the GPU, for b11c768h12nbt3, I used https://github.com/seniorfish/KataGo/releases/tag/v1.17.2-onnx-openvino to run the tests, but it still does not include the configuration file (match, human).
I tried adjusting the parameters, but b40c768nbt still does not work properly on the NPU backend. I’m guessing it’s because it’s too large? Has anyone figured this out? |
Candidate branch to merge to master. Combines #1222 ONNX backend from @seniorfish and #1188 ROCm backend from @Looong01 along with a bunch of followup fixes and refactors I added on top of each backend from local review and testing and a few other features I also worked on recently.
Will go into master if everyone confirms that it's working well.