Skip to content

Backends to merge - #1234

Merged
lightvector merged 52 commits into
masterfrom
backends-to-merge
Aug 14, 2026
Merged

Backends to merge#1234
lightvector merged 52 commits into
masterfrom
backends-to-merge

Conversation

@lightvector

Copy link
Copy Markdown
Owner

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.

seniorfish and others added 30 commits July 31, 2026 12:28
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>
@
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>
@seniorfish

Copy link
Copy Markdown
Contributor

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:

  1. FP32 backends (CPU and DirectML) match the Eigen CPU down to machine precision, with policy squared error around 1e-10 (approx 1.5e-8 average error per point).
  2. OpenVINO FP16 showed minimal noise with policy squared error between 1e-4 and 8e-4, which is noticeably better than previous OpenCL FP16 or MIGraphX FP16 baselines.

I also verified ownership correctness using a custom script calling the kata-raw-nn command across 50 random positions for all models and backends, and all ownership outputs passed well within tolerance(cpu/directml ~1e-6 and openvino-fp16 0.0014–0.0029).

I haven't tested Linux builds yet.

Thanks for working on these refactors!

@Looong01

Copy link
Copy Markdown
Contributor

@lightvector Thanks for our working!

I just do a upgrade for v1.17.2. Just including a very little change: 86c7aaa
Maybe u can have a look!

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 (-DUSE_BACKEND=ROCM -DCMAKE_BUILD_TYPE=Release, 25 archs, make -j8). The ROCm prefix resolution correctly picks /opt/rocm/core-7.14, CK FMHA fused attention is enabled, and RPATH is set correctly. katago version reports v1.17.2 + 66b12ac with the ROCm backend.

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):

Model My branch #1234 tip Delta
b10c384h6nbttflrs 2490 2649 +6.4%
b10c512h8nbt3tflrs-fson-silu-rsnh 1396 1506 +7.9%
kata1-zhizi-b40c768nbt-s11272M-d5935M 336 354 +5.4%
b11c768h12nbt3tflrs-fson-silu 927 780 -15.8%

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:

  • rocmInputsUseNHWC=false gives the same 782 v/s, so the inputsUseNHWC default flip for ROCm in c002c97 is not the cause.
  • With rocmDisableFusedAttention=true: my branch drops to 576, Backends to merge #1234 drops to 493. So the unified backend is ~15% slower for this model on BOTH the plain attention path and the CK FMHA path (my branch 576 -> 927 with CK on, Backends to merge #1234 493 -> 780 with CK on). The regression is not in the CK dispatch but somewhere in the shared trunk path (conv/matmul/buffer handling or MIOpen solution selection) that only hurts the 768-channel pure transformer trunk — the 384/512-channel transformers and the 768-channel hybrid b40 are all unaffected (and faster).

But even with this performance regression, overall it looks good to me and I think it's worth merging.

seniorfish and others added 2 commits August 13, 2026 17:46
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>
@seniorfish

seniorfish commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Should fix #1222 (comment) DirectML issues.

@foxrainowo

foxrainowo commented Aug 13, 2026

Copy link
Copy Markdown

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 gtp_gpu_npu_example.cfg?. As far as I know, NPU is only suitable in 1 thread. Also, it is too fancy that there are many precision can be adjust in gtp_gpu_npu_example.cfg. Can you make it more formal and consistent?

PS C:\Users\foxrain> C:\Users\foxrain\Downloads\katago-onnx-openvino-v4\katago.exe benchmark -model C:\Users\foxrain\Downloads\kata1-zhizi-b40c768nbt-s11272M-d5935M.bin.gz -config C:\Users\foxrain\Downloads\katago-onnx-openvino-v4\gtp_example.cfg -t 1
2026-08-12 12:29:39+0800: Running with following config:
allowResignation = true
lagBuffer = 1.0
logAllGTPCommunication = true
logDir = gtp_logs
logSearchInfo = true
logSearchInfoForChosenMove = false
logToStderr = false
maxTimePondering = 60.0
maxVisits = 500
numSearchThreads = 6
onnxOpenVINODeviceType = NPU
onnxProvider = openvino
ponderingEnabled = false
resignConsecTurns = 3
resignThreshold = -0.90
rules = tromp-taylor
searchFactorAfterOnePass = 0.50
searchFactorAfterTwoPass = 0.25
searchFactorWhenWinning = 0.40
searchFactorWhenWinningThreshold = 0.95

2026-08-12 12:29:39+0800: Loading model and initializing benchmark...
2026-08-12 12:29:39+0800: Testing with default positions for board size: 19
2026-08-12 12:29:39+0800: nnRandSeed0 = 4822533320517784866
2026-08-12 12:29:39+0800: After dedups: nnModelFile0 = C:\Users\foxrain\Downloads\kata1-zhizi-b40c768nbt-s11272M-d5935M.bin.gz useFP16 auto
2026-08-12 12:29:39+0800: Initializing neural net buffer to be size 19 * 19 exactly
2026-08-12 12:29:57+0800: ONNX backend: creating compute context for 19x19 with provider 'openvino'
2026-08-12 12:29:57+0800: ONNX backend thread 0: Model version 15
2026-08-12 12:29:57+0800: ONNX backend thread 0: Model name: kata1-zhizi-b40c768nbt-fdx6d-s11272M-d5935M (nbt convnet, 232532105 params)
2026-08-12 12:29:57+0800: ONNX backend thread 0: provider=openvino deviceIdx=NPU
2026-08-12 12:29:57+0800: ONNX backend: building ONNX graph from model weights...
2026-08-12 12:29:57+0800: Building internal onnx model, requireExactNNLen=true transformerNHWC=false
2026-08-12 12:29:59+0800: ONNX backend: ONNX graph built (930538868 bytes)
2026-08-12 12:29:59+0800: ONNX backend: OpenVINO EP enabled for thread 0, device_type=NPU
2026-08-12 12:30:04+0800: ONNX backend: graph input order: [0]InputSpatial [1]InputGlobal [2]InputMask
2026-08-12 12:30:04+0800: ONNX backend: graph output order: [0]OutputPolicyPass [1]OutputPolicy [2]OutputValue [3]OutputScoreValue [4]OutputOwnership
2026-08-12 12:30:04+0800: ONNX backend: session created, inputs=3 outputs=5
PS C:\Users\foxrain>

@seniorfish

Copy link
Copy Markdown
Contributor

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.
I admit the previous config file options were too confusing, so I streamlined them, removed unnecessary options, set more reasonable defaults, and now the NPU batch is fixed at 1. I also added a warmup phase to improve performance.

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

seniorfish and others added 3 commits August 13, 2026 23:23
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>
@lightvector
lightvector merged commit ccdec95 into master Aug 14, 2026
11 checks passed
@lightvector

Copy link
Copy Markdown
Owner Author

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 NeuralNet::getWarmupBatchSizes(...,maxBatchSize) to nninterface.h so that the backend can report what warmup should be done at what batch sizes, returning empty if the backend doesn't need warmup. Additionally, does the NPU branch really have no benefit for batch size > 1? For example if we let it build its graph for batch size 2 as well, would we get better performance from splitting batches into 2s and then a 1 if odd, instead of splitting all into 1s?

@foxrainowo

foxrainowo commented Aug 14, 2026

Copy link
Copy Markdown

@seniorfish

On the NPU, for b11c768h12nbt3, onnxTransformerNHWC=false provides a 3× speedup; for b28c512nbt and b18c384nbt, onnxSkipScale8=true provides a 4× speedup.

On the GPU, for b11c768h12nbt3, onnxTransformerNHWC=true provides a 3× speedup; for b40c768nbt, b28c512nbt, and b18c384nbt, onnxSkipScale8=true provides a 2× speedup. The performance starts to decline with 15 threads or more, and the speed with 20 threads is below 5 v/s.

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).

katago-onnx-openvino-c025a30 b11c768h12nbt3 b40c768nbt b28c512nbt b18c384nbt b40c256
NPU_t1_onnxSkipScale8=false_onnxTransformerNHWC=false 34 --- 32 61 276
NPU_t1_onnxSkipScale8=false_onnxTransformerNHWC=true 10 --- 32 59 266
NPU_t1_onnxSkipScale8=true_onnxTransformerNHWC=false 35 --- 141 253 249
NPU_t1_onnxSkipScale8=true_onnxTransformerNHWC=true 10 --- 135 244 267
GPU_onnxSkipScale8=false_onnxTransformerNHWC=false 26 30 90 182 334
GPU_onnxSkipScale8=false_onnxTransformerNHWC=true 105 33 88 189 320
GPU_onnxSkipScale8=true_onnxTransformerNHWC=false 29 56 169 427 330
GPU_onnxSkipScale8=true_onnxTransformerNHWC=true 100 56 165 431 321

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?

PS C:\Users\foxrain> C:\Users\foxrain\Downloads\katago-onnx-openvino-c025a30\katago.exe benchmark -model C:\Users\foxrain\Downloads\kata1-zhizi-b40c768nbt-s11472M-d5982M.bin.gz -config C:\Users\foxrain\Downloads\katago-onnx-openvino-c025a30\gtp_example.cfg -t 1
2026-08-14 12:57:10+0800: Running with following config:
allowResignation = true
lagBuffer = 1.0
logAllGTPCommunication = true
logDir = gtp_logs
logSearchInfo = true
logSearchInfoForChosenMove = false
logToStderr = false
maxTimePondering = 60.0
maxVisits = 500
numSearchThreads = 1
onnxDeviceToUse = 0
onnxOpenVINOCacheDir = C://temp//katago_ov_cache
onnxOpenVINODeviceType = NPU
onnxOpenVINOPrecision = FP16
onnxProvider = openvino
onnxSkipScale8 = false
onnxTransformerNHWC = false
ponderingEnabled = false
resignConsecTurns = 3
resignThreshold = -0.90
rules = tromp-taylor
searchFactorAfterOnePass = 0.50
searchFactorAfterTwoPass = 0.25
searchFactorWhenWinning = 0.40
searchFactorWhenWinningThreshold = 0.95

2026-08-14 12:57:10+0800: Loading model and initializing benchmark...
2026-08-14 12:57:10+0800: Testing with default positions for board size: 19
2026-08-14 12:57:10+0800: nnRandSeed0 = 9550974731077147000
2026-08-14 12:57:10+0800: After dedups: nnModelFile0 = C:\Users\foxrain\Downloads\kata1-zhizi-b40c768nbt-s11472M-d5982M.bin.gz useFP16 auto
2026-08-14 12:57:10+0800: Initializing neural net buffer to be size 19 * 19 exactly
2026-08-14 12:57:24+0800: ONNX backend: creating compute context for 19x19 with provider 'openvino'
2026-08-14 12:57:24+0800: ONNX backend thread 0: Model version 15
2026-08-14 12:57:24+0800: ONNX backend thread 0: Model name: kata1-zhizi-b40c768nbt-s11472M-d5982M (nbt convnet, 232532105 params)
2026-08-14 12:57:24+0800: ONNX backend thread 0: provider=openvino deviceIdx=NPU
2026-08-14 12:57:24+0800: ONNX backend: building ONNX graph from model weights...
2026-08-14 12:57:24+0800: Building internal onnx model, requireExactNNLen=true transformerNHWC=false
2026-08-14 12:57:25+0800: ONNX backend: ONNX graph ready (930619286 bytes)
2026-08-14 12:57:25+0800: ONNX backend: OpenVINO execution provider enabled for thread 0, device_type=NPU, cache_dir=C://temp//katago_ov_cache, precision=FP16
2026-08-14 12:57:28+0800: ONNX backend: graph input order: [0]InputSpatial [1]InputGlobal [2]InputMask
2026-08-14 12:57:28+0800: ONNX backend: graph output order: [0]OutputPolicyPass [1]OutputPolicy [2]OutputValue [3]OutputScoreValue [4]OutputOwnership
2026-08-14 12:57:28+0800: ONNX backend: session created, inputs=3 outputs=5
2026-08-14 12:57:28+0800: ONNX backend: OpenVINO warmup for thread 0: precompiling batch sizes 1..1 on device_type=NPU (cache_dir=C://temp//katago_ov_cache, compiled graphs are reused by later runs)
PS C:\Users\foxrain>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants