6.1 KiB
name, description
| name | description |
|---|---|
| ryzenai-oga-model-dir-and-eos-override | Debug OGA on XDNA2 when the RyzenAI EP never claims the If-rooted graph (shadow model dirs, bind-mounted configs, missing device), or a TTS/LLM graph stops on the wrong eos token. |
OGA on XDNA2: graph not claimed, or generation stops at once
Two independent failure families in the AMD onnxruntime-genai (OGA) + RyzenAI EP stack on
Linux XDNA2 (Strix Halo / Ryzen AI Max). Diagnose them in this order — they are separate
gates and each has its own instrument.
Gate 0 — is the NPU path alive at all?
Run the vendor's own instrument before touching anything else:
cd /opt/rai/quicktest && RYZEN_AI_INSTALLATION_PATH=/opt/rai python quicktest.py
# want: "Setting environment for STX/KRK" ... "Session successfully initialized" ... "Test Finished"
It finds the device by shelling out to lspci -nn and grepping 1022:17f0, so it also
proves lspci is present in the container. Pass ⇒ the in-tree amdxdna driver and the
extracted userspace pair correctly; stop blaming the host.
Separate XRT enumeration from OGA's device probe — they are not the same check:
import sys; sys.path.insert(0, "/opt/xilinx/xrt/python")
import pyxrt; dev = pyxrt.device(0) # constructs => XRT sees the device
pyxrt.device(0) succeeding while OGA says No RyzenAI devices detected means the
refusal is OGA's own predicate, inside libonnxruntime-genai.so (the string lives there,
next to RyzenAI, max_length_for_kv_cache, library_path). EP-side env levers worth
pulling: RYZENAI_EP_TRACING, RYZENAI_EP_PERFORMANCE_COUNTERS, RYZENAI_LLM_ASYNC_INIT,
RYZENAI_LLM_PROFILE_INIT.
Gate 1 — invalid model … Node (If) cond_full
This is ONNX Runtime verifying the raw graph, which happens when no provider claimed
it: the EP registered nothing and the CPU provider was left holding an If whose branches
mismatch. It is never a corrupted model.
LD_DEBUG=files LD_DEBUG_OUTPUT=/tmp/ld python loadprobe.py
grep -a "calling init:.*ryzenai\|undefined symbol" /tmp/ld.*
# observed: libonnxruntime_providers_ryzenai.so loaded, then
# symbol lookup error: undefined symbol: GetProvider (fatal)
GetProvider is not a dynamic symbol in the ORT cores, the shared-provider stub, or the
EP (nm -D shows neither defines nor wants it), so the failure is registration, not an
ELF mismatch. Confirm the cores are identical before chasing versions: both shipped
libonnxruntime.so.1.27.0 copies hash equal.
Separate the two gates with an explicit global-scope load before importing OGA:
import ctypes
ctypes.CDLL("/opt/rai/deployment/lib/libonnxruntime_providers_ryzenai.so",
mode=ctypes.RTLD_GLOBAL)
import onnxruntime_genai as og
og.Model(MODEL) # registration fixed => error MOVES to "No RyzenAI devices detected"
An error that moves proves you cleared one gate and exposes the next. Silence in the first form means the provider list was empty.
Dead ends (measured, do not retry)
- Shadow model dirs never work. Hardlink mirror, symlink mirror, and even a
read-only single-file bind mount of a patched
genai_config.jsonover the export's owngenai_config.jsonall produce the sameIfrejection. The graph is only claimable when loaded from the export's own directory, and there is no config-path override in0.14.0-dev: the only env names in the shipped binaries areGENAI_LOG_ORT_LIB,GENAI_ORT_VERBOSE_LOGGING,OG_ORT_LIB. Carrying a config change therefore means either editing the download in place or a single-file bind mount over the real dir (-v file.json:/spike/<model>/genai_config.json:ro) — and if the EP still refuses, the gate is not the config. - Bisect the image before believing "environment drift". Run the same load probe against every local tag; three tags failing identically rules the image out.
- Rule out host state with instruments, not memory:
fuser -v /dev/accel/accel0,dmesg | grep -i xdna,stat -c "%i %y"on the model files,df -h,free -g. sysfsclass isaccel, notmisc: glob/sys/class/accel*/*. An empty/sys/class/misc/accel*is your glob, not a missing device.GENAI_ORT_VERBOSE_LOGGING=1andog.set_log_options(...)produce nothing useful in this build;set_log_optionsis a pybind builtin (noinspect.signature) and rejectsseverityas an int. Don't burn turns trying to make it talk — useLD_DEBUG.straceis absent from the RAI image andaptmay have no mirror:LD_DEBUG=filesis the loader tracer that needs nothing installed.
Gate 2 — generation stops after one token
OGA reads its stop list from the model section: model.eos_token_id. Chat exports
carry the chat-turn-close id (e.g. 151645) there, so the model emits it first and the
speech/tool budget is never spent — while search has no eos key: writing
search.eos_token_id is a parse refusal (Unknown value "eos_token_id"), not a no-op.
- Fix: rewrite
model.eos_token_idto only the task's own end token (TTS: the speech-generation-end id), or pass it throughset_search_optionsif the build accepts that key name. - A sub-millisecond
generate_next_token()with finite, high-confidence logits means the graph ran and chose a stop token — it is not evidence of a CPU fallback. - Verify a token-range census against the export's
added_tokens.json, not against a guessed vocabulary size: NeuCodec codes reach 65,536 entries (ids 151671…217206), and a census bounded at 10k silently miscounts every code as a text token. - A hardlinked run dir is a loaded gun:
open(dst, "w")on a hardlink truncates the original through the shared inode.os.unlinkthe link before writing a patched file. Also skip.git/and mirror directories when walking an HF checkout.
Rate measurement
Once a load succeeds, measure at ≥300 steps with a host-side %CPU sampler beside the
container (ps -C python -o pid=,pcpu=,rss= every 2 s): a device-bound decode waits on
accel0 and stays under one core, a silent CPU fallback pegs at least one. Never report
request #1 after a boot — graph capture makes it a warmup artifact.