Skip to content

feat(phi4): run Phi-4-mini-instruct Q8_0 GGUF through ryzenai-corelib - #748

Open
chizamd wants to merge 17 commits into
ROCm:mainfrom
chizamd:feature/phi4-rai
Open

chizamd wants to merge 17 commits into
ROCm:mainfrom
chizamd:feature/phi4-rai

Conversation

@chizamd

@chizamd chizamd commented Sep 22, 2026 •

Copy link
Copy Markdown

Runs Phi-4-mini-instruct from a Q8_0 GGUF, through AMD's
ryzenai-corelib. The weights are read straight from the GGUF — no ONNX
model, no tensor manifest, no converted weight file is produced or
shipped.

There is no separate tag for it. phi4-mini-it:4b resolves to the
artifacts the build's NPU generation can run: the NPU2/Q4NX entry on
stx, this GGUF entry on aie_next. flm list on a rai build shows only
what it can run.

112 files, +12,221 / −1,874 vs 0cfecb08. Packaging and CI are still
absent — see the known limitations at the end.


The file is memory-mapped and every tensor handed out as a span —
nothing copied, converted or cached to disk. RequireQ8/RequireF32
take a name and an expected shape, so all 161 tensors plus the rope
factors are resolved and shape-checked before the first device object
exists
. Head counts, rope base and the short-factor table come from
the file's own metadata; the architecture is a fixed C++ constant block
that the GGUF metadata, config.json and tokenizer_config.json are
cross-checked against. A mismatched package fails at load with an exact
diagnostic, never mid-generation.

tokenizer_config.json is parsed once by the frontend and passed down
through BackendContext, so the model directory layout stays the
frontend's knowledge and a backend never opens the directory itself.

Requantizing the 161 weights from Q8_0 to group-64 is effectively all of
model load. Two things address it.

The creates are independent — each reads its own mapped range and
produces its own object — so they run across a pool of 8. The per-create
thread hint stays at corelib's default of one so the two forms of
parallelism don't multiply. The pool is threads FastFlowLM owns, so it
is unaffected by thread limits the surrounding environment may impose on
the packer.

On top of that, the packed bytes are cached. All 161 blobs go in one
file with a JSON index written last and renamed into place, so a data
file without a matching index is never used. Both directions walk the
same slot order, so an index entry always refers to the weight it was
written from — the creates finish out of order but are assigned by slot,
not by completion. The key is the GGUF's size and write time, the
corelib version, the group size and the weight count; not the GGUF's
content, because hashing 4 GB costs more than the packing the cache
exists to avoid. That is safe here because corelib independently rejects
a slice whose length isn't exactly what the descriptor packs to, so a
stale-but-plausible cache cannot quietly become the wrong weights. A
miss, a stale key, an unreadable index or a failed bind all fall back to
packing, and a partial bind releases everything rather than leaving half
a model. A cache that no longer matches is deleted when that is known,
not whenever the next write happens to succeed. FLM_RAI_WEIGHT_CACHE
redirects it, or disables it with 0.

Every device tensor — hidden/residual/skip, Q, K, attention, the 32 K
and V caches, rope tables — is created once at load, sized to the
maximum padded extents the shape plan interrogates out of corelib's own
padding helpers. The plan is queried only at corelib's nine real
execution buckets (1, 64, 128, 256, 512, 1024, 2048, 3072, 4096) and
every logical row maps to its next bucket, so nothing re-enters the
helpers on the hot path and nothing allocates per token.

Per layer the device does everything except the embedding gather: Q/K
matmul, V matmul written directly into a tensor window of the V cache
at the current position
(no host-side KV scatter), flat_mha against
the caches with the rope tables, O projection, fused SSMLP with a
residual/skip ping-pong. Two synchronizes per token — one after all
32 layers, one after the head — not one per layer.

The NPU generation is not detected. A build carries engines for exactly
one of them, FLM_ENABLE_RAI picks which at compile time, and
utils::build_npu_platform() is a constexpr that reports the choice.
The catalog in model_list.json declares which generations each model
supports and model_list prunes itself to the one the build was made
for, so flm list works on a machine with no NPU without asking the
hardware anything.

Backend selection is separate and explicit: a build links one kernel
flow, and that is the default, never a filename or a quantization level.
The catalog does not name a backend — details.execution_backend is
retired, and a test asserts it stays that way. Once this entry is selected there is
no fallback — a missing, unloadable or wrong-version corelib fails
the load with a diagnostic rather than quietly running on CPU or NPU2.
A rai build links corelib in, because the NPU device the process
shares comes from corelib's GetDevice(); an exact 0.5.0 match is
required while corelib is pre-1.0, enforced at compile time and again at
run time. Both checks read the vendor header's own
RYZENAI_CORELIB_VERSION_* macros rather than repeating the triple, so
the compile-time #error, the run-time comparison, the fake, the tests
and the weight-cache key cannot drift apart on the next bump.

Once work has reached the device, any throw drains with a synchronize
and marks the engine poisoned; every later entry point refuses
rather than continuing on indeterminate state.

File Source Revision
Phi-4-mini-instruct.Q8_0.gguf unsloth/Phi-4-mini-instruct-GGUF
78eb92a4
tokenizer.json, tokenizer_config.json, config.json
microsoft/Phi-4-mini-instruct cfbefacb

All four are SHA-256 verified before the download is promoted.
Verification is a pull-time concern: flm pull and flm check verify
in full, while run and serve check presence and version rather than
re-hashing 4 GB on every launch.

The usable window is 4095 tokens (prompt + output), so the largest
admissible prompt is 4094; over-capacity requests are rejected with HTTP
400 before submission. The 4096 ceiling is a correctness boundary, not a
buffer size: it is Phi-4-mini's rope.scaling.original_context_length,
LongRoPE selects factors by sequence length, and only the short branch
is derived here — enforced at load, which fails unless the GGUF reports
exactly 4096. The further step down to 4095 is this frontend's own
conservatism so an admitted request can always finish; it is not imposed
by corelib.

Model loaded in N s on both the CLI and server load paths.
FLM_RAI_PROFILE_LOAD=1 breaks the rai load into shape planning, GGUF
resolution, host preparation, weight requantization and device
allocation, and says whether the weights came from the cache, what
writing one cost, and how much stale cache was reclaimed. Decode timing
is recorded on this backend, so /status, /verbose and the
Ollama-compatible eval_duration report real figures.

corelib 0.5.0 moved three things that reach into this engine. The row
count left every dispatch: M now comes from the operand's own shape, and
each operand is checked against its OWN padded extent rather than through
the allocation it sits in -- so a forward opens one view per operand per
op, at the extent the plan interrogated for it, over device tensors that
are still created once at load. The padding helpers gained the stream,
because the PDI pair a stream was opened with selects the kernel set and
a shape can exist under one pair and not the other; the shape plan is
therefore built after the stream rather than in the constructor's
initializer list. And the PDI pair itself is now required and has no
default: Phi-4 is one of the older families, so p1 prefill / p16 token,
pinned in phi4_rai_constants.hpp with the rest of the model's facts.
has_device_context() became get_device(), NULL for no NPU.


Three standalone suites, all hardware-free except where noted:

  • phi4_rai — 9 targets on a fake corelib implementing
    the 0.5.0 C ABI in-process plus a deterministic GGUF v3 fixture
    builder
    : ABI/version/path/lifetime, parsing and corruption, host ops,
    shape plan, engine load and dispatch sequencing with object-leak and
    concurrency checks, weight-cache round trip / staleness / reclaim,
    frontend routing and request lifecycle, a compile-gate proving the OFF
    build carries no corelib, and the downloader including legacy-entry
    compatibility. 9/9 on an aie_next host, with test_real_corelib
    executing against the real corelib DLL rather than skipping; 8/9
    elsewhere, where that one test skips for want of hardware.
  • model_backend — the backend registry and resolution rules, on
    stubs. 9/9.
  • model_list_platform — catalog platform filtering and override
    merging. 6/6.

run_real_rai_acceptance.ps1 drives the hardware matrix end to end and
writes a machine-readable record. Its provenance step degrades to
unknown when a revision cannot be read rather than failing the run:
it used to call .Trim() on the output of a git rev-parse that had
printed nothing, which took the whole matrix down after it had already
passed.


Two aie_next machines, measuring different things.

Machine A — full acceptance matrix, passed: true, 0 failures:
pinned pull/check; 10/10 fresh-process CLI load-and-generate cycles;
/api/chat and /v1/chat/completions both 200, streaming and
non-streaming; cancellation clean with the next request served; 4095
admitted and 4096 rejected with HTTP 400 before submission; no CPU or
NPU2 fallback in any log. Generation figures: cold TTFT 4.21 s, warm
TTFT 65.0 ms, decode 21.3 tok/s over REST and 35.8 tok/s in a warm CLI
session.

Machine B (a 3-column aie_next part) — model load, with the current
packer:

Weight phase Range
Packing (no cache) 3.50 – 13.51 s, median 8.76
Cache hit 0.07 – 3.57 s, median 3.56

First load additionally writes a 2.0 GB cache in ~1–2 s. The 50× spread
within cache hits is page-cache warmth. Load on this machine varies
about 4× run to run on an otherwise idle box, so these are ranges rather
than single figures — a single sample of this phase is not meaningful.

docs/docs/benchmarks/phi4_results.md keeps the two machines separate
and says plainly which figures were measured where, and which were not
re-measured after the packer changed.


  1. Greedy decoding is not reproducible across loads. Eight loads of
    the same model answering the same prompt at temperature=0, top_k=1
    produced 2 distinct answers; serialising the packer produced 3, so this
    is not caused by the concurrent creates. It is consistent with the
    requantization refit not being bit-reproducible, and is expected to
    disappear when dedicated ELFs remove the refit.
  2. The acceptance matrix was re-run in full after the move to corelib
    0.5.0, on an aie_next host: passed: true, 0 failures, 10/10
    fresh-process CLI load-and-generate cycles, REST both streaming and
    non-streaming, cancellation clean, the capacity boundary rejected
    before submission. Cold TTFT 4.65 s, warm TTFT 35.1 ms, decode 34.6
    tok/s. The three unit suites are 11/11 on the same box, with
    test_real_corelib running against the real DLL rather than skipping.
  3. calculate_file_sha256 is ~8× slower than the work requires —
    ~28 s over 4 GB where Get-FileHash takes 3.67 s, because it uses a
    portable pure-C++ SHA-256. No longer on the startup path, but every flm pull and flm check pays it, for every model. Shared pull code,
    unrelated to this backend, left for its own change.
  4. A shared FLM_RAI_WEIGHT_CACHE directory holds one cache, so
    alternating two models there repacks each time. The default — beside the
    model — does not have this.
  5. No packaging. MSI, WiX and Inno are untouched; this is a
    rai-enabled developer build.
  6. No CI builds or runs any of this, and the release preset does not
    compile the rai path.
  7. Windows only, Q8_0 only — no Q4_0/Q4_K/Q6_K, no mixed
    quantization, no generic GGUF runtime, no other model family.
  8. Usable context is 4095, against 128k for the model and 32k for
    the stx entry.

#706 reaches the same backend through an **ONNX manifest

  • packed weights** pipeline and carries packaging this branch does not.
    This is the GGUF-direct approach: smaller surface, no manifest
    generator, no converted artifacts, no Python at runtime. They are
    alternatives; only one should land.

Co-authored-by: Claude Opus 5 (1M context) noreply@anthropic.com
Co-authored-by: alfxu_amdeng Alfred.Xu@amd.com


🤖 Generated with Claude Code

@chizamd
chizamd force-pushed the feature/phi4-rai branch 6 times, most recently from a2842c7 to 23c77b1 Compare September 22, 2026 08:57
@chizamd
chizamd marked this pull request as ready for review September 22, 2026 08:57
Runs **Phi-4-mini-instruct from a Q8_0 GGUF**, through AMD's
`ryzenai-corelib`. The weights are read straight from the GGUF — no ONNX
model, no tensor manifest, no converted weight file is produced or
shipped.

There is **no separate tag for it**. `phi4-mini-it:4b` resolves to the
artifacts the build's NPU generation can run: the NPU2/Q4NX entry on
`stx`, this GGUF entry on `aie_next`. `flm list` on a rai build shows only
what it can run.

112 files, +12,221 / −1,874 vs `0cfecb08`. Packaging and CI are still
absent — see the known limitations at the end.

---

The file is memory-mapped and every tensor handed out as a `span` —
nothing copied, converted or cached to disk. `RequireQ8`/`RequireF32`
take a name *and* an expected shape, so all 161 tensors plus the rope
factors are resolved and shape-checked **before the first device object
exists**. Head counts, rope base and the short-factor table come from
the file's own metadata; the architecture is a fixed C++ constant block
that the GGUF metadata, `config.json` and `tokenizer_config.json` are
cross-checked against. A mismatched package fails at load with an exact
diagnostic, never mid-generation.

`tokenizer_config.json` is parsed once by the frontend and passed down
through `BackendContext`, so the model directory layout stays the
frontend's knowledge and a backend never opens the directory itself.

Requantizing the 161 weights from Q8_0 to group-64 is effectively all of
model load. Two things address it.

The creates are independent — each reads its own mapped range and
produces its own object — so they run across a pool of 8. The per-create
thread hint stays at corelib's default of one so the two forms of
parallelism don't multiply. The pool is threads FastFlowLM owns, so it
is unaffected by thread limits the surrounding environment may impose on
the packer.

On top of that, the packed bytes are cached. All 161 blobs go in one
file with a JSON index written last and renamed into place, so a data
file without a matching index is never used. Both directions walk the
same slot order, so an index entry always refers to the weight it was
written from — the creates finish out of order but are assigned by slot,
not by completion. The key is the GGUF's size and write time, the
corelib version, the group size and the weight count; **not** the GGUF's
content, because hashing 4 GB costs more than the packing the cache
exists to avoid. That is safe here because corelib independently rejects
a slice whose length isn't exactly what the descriptor packs to, so a
stale-but-plausible cache cannot quietly become the wrong weights. A
miss, a stale key, an unreadable index or a failed bind all fall back to
packing, and a partial bind releases everything rather than leaving half
a model. A cache that no longer matches is deleted when that is known,
not whenever the next write happens to succeed. `FLM_RAI_WEIGHT_CACHE`
redirects it, or disables it with `0`.

Every device tensor — hidden/residual/skip, Q, K, attention, the 32 K
and V caches, rope tables — is created once at load, sized to the
maximum padded extents the shape plan interrogates out of corelib's own
padding helpers. The plan is queried only at corelib's nine real
execution buckets (1, 64, 128, 256, 512, 1024, 2048, 3072, 4096) and
every logical row maps to its next bucket, so nothing re-enters the
helpers on the hot path and nothing allocates per token.

Per layer the device does everything except the embedding gather: Q/K
matmul, V matmul written **directly into a tensor window of the V cache
at the current position** (no host-side KV scatter), `flat_mha` against
the caches with the rope tables, O projection, fused SSMLP with a
residual/skip ping-pong. **Two synchronizes per token** — one after all
32 layers, one after the head — not one per layer.

The NPU generation is not detected. A build carries engines for exactly
one of them, `FLM_ENABLE_RAI` picks which at compile time, and
`utils::build_npu_platform()` is a `constexpr` that reports the choice.
The catalog in `model_list.json` declares which generations each model
supports and `model_list` prunes itself to the one the build was made
for, so `flm list` works on a machine with no NPU without asking the
hardware anything.

Backend selection is separate and explicit: a build links one kernel
flow, and that is the default, never a filename or a quantization level.
The catalog does not name a backend — `details.execution_backend` is
retired, and a test asserts it stays that way. Once this entry is selected there is
**no fallback** — a missing, unloadable or wrong-version corelib fails
the load with a diagnostic rather than quietly running on CPU or NPU2.
A rai build links corelib in, because the NPU device the process
shares comes from corelib's `GetDevice()`; an exact `0.5.0` match is
required while corelib is pre-1.0, enforced at compile time and again at
run time. Both checks read the vendor header's own
`RYZENAI_CORELIB_VERSION_*` macros rather than repeating the triple, so
the compile-time `#error`, the run-time comparison, the fake, the tests
and the weight-cache key cannot drift apart on the next bump.

Once work has reached the device, any throw drains with a synchronize
and marks the engine **poisoned**; every later entry point refuses
rather than continuing on indeterminate state.

| File | Source | Revision |
|---|---|---|
| `Phi-4-mini-instruct.Q8_0.gguf` | `unsloth/Phi-4-mini-instruct-GGUF` |
`78eb92a4` |
| `tokenizer.json`, `tokenizer_config.json`, `config.json` |
`microsoft/Phi-4-mini-instruct` | `cfbefacb` |

All four are SHA-256 verified before the download is promoted.
Verification is a pull-time concern: `flm pull` and `flm check` verify
in full, while run and serve check presence and version rather than
re-hashing 4 GB on every launch.

The usable window is **4095** tokens (prompt + output), so the largest
admissible prompt is 4094; over-capacity requests are rejected with HTTP
400 before submission. The 4096 ceiling is a correctness boundary, not a
buffer size: it is Phi-4-mini's `rope.scaling.original_context_length`,
LongRoPE selects factors by sequence length, and only the short branch
is derived here — enforced at load, which fails unless the GGUF reports
exactly 4096. The further step down to 4095 is this frontend's own
conservatism so an admitted request can always finish; it is not imposed
by corelib.

`Model loaded in N s` on both the CLI and server load paths.
`FLM_RAI_PROFILE_LOAD=1` breaks the rai load into shape planning, GGUF
resolution, host preparation, weight requantization and device
allocation, and says whether the weights came from the cache, what
writing one cost, and how much stale cache was reclaimed. Decode timing
is recorded on this backend, so `/status`, `/verbose` and the
Ollama-compatible `eval_duration` report real figures.

corelib 0.5.0 moved three things that reach into this engine. The row
count left every dispatch: M now comes from the operand's own shape, and
each operand is checked against its OWN padded extent rather than through
the allocation it sits in -- so a forward opens one view per operand per
op, at the extent the plan interrogated for it, over device tensors that
are still created once at load. The padding helpers gained the stream,
because the PDI pair a stream was opened with selects the kernel set and
a shape can exist under one pair and not the other; the shape plan is
therefore built after the stream rather than in the constructor's
initializer list. And the PDI pair itself is now required and has no
default: Phi-4 is one of the older families, so p1 prefill / p16 token,
pinned in phi4_rai_constants.hpp with the rest of the model's facts.
`has_device_context()` became `get_device()`, NULL for no NPU.

---

Three standalone suites, all hardware-free except where noted:

- **`phi4_rai`** — 9 targets on a **fake corelib** implementing
the 0.5.0 C ABI in-process plus a deterministic **GGUF v3 fixture
builder**: ABI/version/path/lifetime, parsing and corruption, host ops,
shape plan, engine load and dispatch sequencing with object-leak and
concurrency checks, weight-cache round trip / staleness / reclaim,
frontend routing and request lifecycle, a compile-gate proving the OFF
build carries no corelib, and the downloader including legacy-entry
compatibility. **9/9 on an aie_next host**, with `test_real_corelib`
executing against the real corelib DLL rather than skipping; 8/9
elsewhere, where that one test skips for want of hardware.
- **`model_backend`** — the backend registry and resolution rules, on
stubs. 9/9.
- **`model_list_platform`** — catalog platform filtering and override
merging. 6/6.

`run_real_rai_acceptance.ps1` drives the hardware matrix end to end and
writes a machine-readable record. Its provenance step degrades to
`unknown` when a revision cannot be read rather than failing the run:
it used to call `.Trim()` on the output of a `git rev-parse` that had
printed nothing, which took the whole matrix down after it had already
passed.

---

Two aie_next machines, measuring different things.

**Machine A** — full acceptance matrix, `passed: true`, 0 failures:
pinned `pull`/`check`; 10/10 fresh-process CLI load-and-generate cycles;
`/api/chat` and `/v1/chat/completions` both 200, streaming and
non-streaming; cancellation clean with the next request served; 4095
admitted and 4096 rejected with HTTP 400 before submission; no CPU or
NPU2 fallback in any log. Generation figures: cold TTFT 4.21 s, warm
TTFT 65.0 ms, decode 21.3 tok/s over REST and 35.8 tok/s in a warm CLI
session.

**Machine B** (a 3-column aie_next part) — model load, with the current
packer:

| Weight phase | Range |
|---|---|
| Packing (no cache) | 3.50 – 13.51 s, median 8.76 |
| Cache hit | 0.07 – 3.57 s, median 3.56 |

First load additionally writes a 2.0 GB cache in ~1–2 s. The 50× spread
within cache hits is page-cache warmth. Load on this machine varies
about 4× run to run on an otherwise idle box, so these are ranges rather
than single figures — a single sample of this phase is not meaningful.

`docs/docs/benchmarks/phi4_results.md` keeps the two machines separate
and says plainly which figures were measured where, and which were not
re-measured after the packer changed.

---

1. **Greedy decoding is not reproducible across loads.** Eight loads of
the same model answering the same prompt at `temperature=0, top_k=1`
produced 2 distinct answers; serialising the packer produced 3, so this
is **not** caused by the concurrent creates. It is consistent with the
requantization refit not being bit-reproducible, and is expected to
disappear when dedicated ELFs remove the refit.
2. **The acceptance matrix was re-run in full** after the move to corelib
0.5.0, on an aie_next host: `passed: true`, 0 failures, 10/10
fresh-process CLI load-and-generate cycles, REST both streaming and
non-streaming, cancellation clean, the capacity boundary rejected
before submission. Cold TTFT 4.65 s, warm TTFT 35.1 ms, decode 34.6
tok/s. The three unit suites are 11/11 on the same box, with
`test_real_corelib` running against the real DLL rather than skipping.
3. **`calculate_file_sha256` is ~8× slower than the work requires** —
~28 s over 4 GB where `Get-FileHash` takes 3.67 s, because it uses a
portable pure-C++ SHA-256. No longer on the startup path, but every `flm
pull` and `flm check` pays it, for every model. Shared pull code,
unrelated to this backend, left for its own change.
4. **A shared `FLM_RAI_WEIGHT_CACHE` directory holds one cache**, so
alternating two models there repacks each time. The default — beside the
model — does not have this.
5. **No packaging.** MSI, WiX and Inno are untouched; this is a
rai-enabled developer build.
6. **No CI** builds or runs any of this, and the release preset does not
compile the rai path.
7. **Windows only, `Q8_0` only** — no Q4_0/Q4_K/Q6_K, no mixed
quantization, no generic GGUF runtime, no other model family.
8. **Usable context is 4095**, against 128k for the model and 32k for
the stx entry.

ROCm#706 reaches the same backend through an **ONNX manifest
+ packed weights** pipeline and carries packaging this branch does not.
This is the **GGUF-direct** approach: smaller surface, no manifest
generator, no converted artifacts, no Python at runtime. They are
alternatives; only one should land.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: alfxu_amdeng <Alfred.Xu@amd.com>
alfxu-amd and others added 16 commits September 23, 2026 14:00
FLM_ENABLE_RAI was rejected at configure time on anything but Windows, and
the code behind the gate had grown three Win32-only spots to match. Nothing
about the backend is actually Windows-specific: ryzenai-corelib's own C++
already carries POSIX branches throughout, and DynamicDispatch builds on
Linux today. Only FastFlowLM's side was holding it shut.

- CMakeLists: drop the FATAL_ERROR, search lib64/ beside lib/ for the
  corelib import library, and make the Boost lookup Windows-only. Boost is
  needed only because MSVC reports __cplusplus as 199711L without
  /Zc:__cplusplus, which sends xrt/detail/any.h down its boost::any branch;
  GCC and Clang report C++20 and never take it.
- CMakeLists: flm_rai named only the singular XRT_INCLUDE_DIR, which the
  pkg-config path never sets, so the target would have had no XRT headers at
  all on Linux. It now picks the same variable the flm target does, and links
  CMAKE_DL_LIBS for the dlopen path that still compiles in a static build.
- phi4_rai_gguf.cpp: mmap the GGUF through open/fstat/mmap where the Win32
  file-mapping calls were the only option. Read-only and MAP_PRIVATE, with
  O_CLOEXEC so a mapping that lives as long as the engine cannot leak into a
  spawned process.
- corelib_api.cpp: Load() dlopens instead of throwing "requires Windows".
  RTLD_LOCAL keeps corelib's symbols out of the global namespace, since this
  process links XRT directly and a corelib built against a different one must
  not satisfy our calls; RTLD_NOW surfaces a missing symbol next to the path
  that was loaded rather than at first dispatch. The three inline ".dll"
  spellings collapse into two constants, so FLM_RAI_CORELIB_PATH diagnostics
  cannot describe a suffix other than the one required.
- Add a linux-rai preset mirroring windows-rai, and update the docs that
  asserted the backend was Windows-only.

Verified by configuring and building on Linux against corelib 0.5.0 headers:
flm_rai compiles all eight translation units, and the flm target compiles
clean with main.cpp's FLM_ENABLE_RAI path wired to it. No NPU hardware here,
so nothing is run; src/test/phi4_rai stays Windows-gated and untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Building the rai backend on Linux worked; installing one did not. Seven
separate faults, none of which report themselves as what they are.

The build could not find corelib. Its headers live outside the
repository, so a rai configure needs to be told where they are once;
CMakeLists.txt now takes RYZENAI_CORELIB_ROOT and looks in src/lib for
the library, which is where an out-of-tree corelib build is staged. The
linux-rai preset described a rai build without enabling one, so
linux-rai-on sets FLM_ENABLE_RAI and stops the description from being
the only thing that says "rai".

home_install.sh then wedged itself. A configure that fails still writes
CMakeCache.txt but never reaches build.ninja, and the guard read the
cache alone as "already configured" -- so every later run skipped the
configure it needed and died with "ninja: error: loading 'build.ninja'",
an error no amount of re-running clears. Key the decision on the
generator file, which only a successful configure writes, and use
--fresh so a retry does not inherit what it is replacing. A cache
configured the other way round was reused just as silently, installing a
stock build for --rai and the reverse; compare the cached
FLM_ENABLE_RAI against what was asked for, unless the preset was named
by hand, since a custom preset's intent is not ours to infer. BUILD_DIR
defaulted to the repository root while every preset's binaryDir is
src/build, so this script and `cmake --build --preset` were building two
separate trees of the same project.

A rai prefix carries corelib and the DynamicDispatch core it links
against on top of the usual engines, which is roughly 550M -- too much
for a home directory. --rai now installs to /scratch/$USER/flm_exe_rai,
falling back to ~/flm_exe_rai where there is no scratch, with FLM_PREFIX
still overriding either.

Finally the staging copied corelib plus whatever ldd reported, which is
the wrong closure. DynamicDispatch was configured with
DD_MDS_IN_BINS_DLL=ON, so the entire AIE4 kernel set lives in
libdyn_bins.so, and nothing declares it DT_NEEDED:
Transaction::load_large_txn_ops_dll() dladdr()s itself and dlopen()s the
file from beside libdyn_dispatch_core.so. ldd cannot see it, and when it
is missing that loader returns silently, leaving an empty shape table
and a first matmul that dies with "Shape list size: 0" -- which reads
like an unsupported model rather than a missing file. Remember the
directory libdyn_dispatch_core.so came from and stage libdyn_bins.so out
of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
XRT does not reach its own plugins through DT_NEEDED. It dlopens
libxrt_core and the libxrt_driver_xdna NPU driver from a path it builds
as $XILINX_XRT/lib/x86_64-linux-gnu/<lib>, and when XILINX_XRT is unset
it guesses that root three directories above wherever libxrt_coreutil
was loaded from. A second XRT anywhere on LD_LIBRARY_PATH aims the
guess at a directory holding no plugins, and nothing says so: the NPU
just comes up with no driver. corelib reports

    corelib: no AIE4 hw_context (unordered_map::at)

and the direct open added in 8c38ad4f then fails too, with

    No such library '/home/alfxu/lib/x86_64-linux-gnu/libxrt_core.so.2'

naming a directory that was never an XRT install. The rai build is the
one that gets hit, because it bundles no XRT of its own.

Resolve the root at install time instead, accepting a candidate only if
the directory XRT will actually dlopen from exists, and export it from
flm_env.sh so the guess never runs. An XRT that ships setup.sh still
speaks for itself; the new path covers the distro layout, where the
libraries sit under /usr and the old /opt/xilinx/xrt default resolved
to an empty directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three faults on the way from "the NPU did not come up" to something a
user can act on.

The engines were handed nullptr. corelib holds the device, so a rai
build has to pass corelib's own xrt::device down rather than the null
one the frontend starts with.

When the runtime failed to come up at all, the reason went to
DO_VERBOSE. VERBOSE is a compile-time macro, so on a release build those
lines are compiled out and no flag brings them back; what the user gets
instead is

  [ERROR]  NPU device instance is nullptr

raised much later, from whichever model first reaches for the device --
a symptom several steps from the cause. Keep the reason in a string and
report it once, up front, for the commands that cannot run without a
device.

And corelib having no device took down far more than rai. It gets its
device from a bare AIE4 hw_context; when that cannot be created, DD
swallows the XRT error in a catch(...) and then throws unordered_map::at
on the key it never inserted, corelib reports "no AIE4 hw_context", and
get_device() goes null for the whole process. But a build with
FLM_ENABLE_RAI still serves every other family through the flm backend,
which opens its own device and never touches corelib. Open a device
directly when corelib has none, and say so:

  [FLM]  rai backend unavailable (corelib unavailable: corelib has no
         device context); opened the NPU directly

This does not bring back the two-device bug, where a buffer created
against a second xrt::device for the same NPU bound without error and
then never completed: that needed corelib live and holding a device of
its own, and the fallback only runs when it has none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adding the corelib backend took the FastFlowLM models away: the catalog
had one notion of "supported", the build had another, and whichever was
asked last won. The fix took several passes -- a per-flavour catalog, a
backend key on every entry -- and this is where they landed, as one
change, because the intermediate shapes were never true of the tree for
long enough to be worth bisecting through.

Two questions are asked of every catalog entry, and they are independent.

Which silicon the entry's artifacts were built for is "supported_platforms",
aie2p or aie_next, checked against utils::get_device(). Every one of the
42 FastFlowLM entries names aie2p; the corelib one names aie_next. A
missing key still means every generation, so a catalog written before the
key keeps working.

Which kernel flow they need is the family name: a family ending in -rai
is ryzenai-corelib's, everything else is FastFlowLM's, checked against
the flows this build actually linked. The flow is derived, never
authored -- model_list stamps "backend" onto the entry from the tag it
was found under, which is where AutoModel already looks, so a
hand-written one would only be a second place to be wrong. --backend and
FLM_BACKEND still win over it.

An entry failing either is dropped before anything downstream sees it,
because a model that is listed but cannot run is worse than one that is
not listed. The generation is asked first and of everyone: linking
corelib says what kernels the binary has and nothing about the silicon it
is on, so a corelib build does not get to see a corelib model merely by
existing.

That makes the corelib phi4 its own top-level family, phi4-mini-it-rai,
beside phi4-mini-it rather than patching it through platform_overrides.
Two packages that share nothing but a lineage -- a Q8_0 GGUF against an
NPU2/Q4NX build -- now read as two entries in one model_list.json, the
merge-patch machinery is gone, and the separate tag means model_info.json
needs no model_info_key redirect to reach the corelib records.

get_device() is a stand-in that consults nothing at all: not the
environment, not FLM_ENABLE_RAI, just default_npu_platform(). One
constant is the whole answer, so there is exactly one thing for the real
probe to replace. It is aie_next for now, while corelib is what is being
brought up -- which means a build without corelib lists no models, since
every FastFlowLM entry names aie2p. That state is reached on purpose and
is not fatal: model_list names the generation and the linked kernels in
one line, then each command fails on its own terms, and `flm --help`
still works. Setting the constant back to aie2p restores the 42.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Model code sat at models/<model>/, with rai sources at models/<model>/rai/.
That encodes at most one of the two axes a model actually varies over, and
encodes neither for the flm engines: nothing in the path said which silicon
a header was written against. With aie_next and rai both arriving, a second
provider or a second generation for a family had nowhere to go but a
conditional.

Both axes now appear in the path, family first:

    models/<model>/<backend>/<platform>/

So models/phi4/flm/aie2p/phi4_npu.hpp beside models/phi4/rai/aie_next/
phi4_rai.hpp -- one family directory holding both providers, each naming the
generation it was written for. Neither axis is derived from the other; the
path just records both answers.

Pure relocation: 43 files moved as renames, every #include and build
reference repointed, no code changed. models_sources.cmake globs
*/rai/*/*.cpp to reach through the new platform level, and the layout
sections of common/models/README.md and create_new_model.md describe the
three levels.

Verified: cmake configure + ninja build clean with -DFLM_ENABLE_RAI=ON
(64/64, flm links and runs), the glob picks up all six rai sources, and
every models/ include resolves to a file on disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
40aa9fe reworded the unregistered-backend error to "is not available for
model family '<family>'. It provides: ..." and moved test_model_backend to
match, but test_phi4_frontend still looked for the old "not compiled into
this build", so both its ON and OFF builds failed.

The OFF case also checks the provider list is exactly flm, which is the
point of that test: a build without corelib registers no rai engine.

Co-authored-by: Cursor <cursoragent@cursor.com>
_chunked_insert took the last chunk's logits with `y = chunk_y`. buffer's
copy assignment is shallow and does not take ownership, so once chunk_y
went out of scope at the end of the loop body, y pointed at freed memory
and the first token of a reply was sampled from it.

The flm engines never showed it: they return views of memory they keep.
The rai phi4 engine returns logits it owns, so on rai the first token came
from whatever the heap had put there -- a first reply of "!4" to "What is
2+2?", and fresh processes answering with "!", "%", "&hellip;" or "![](...".
It now moves, which for a non-owning buffer is the same as the copy was.

The phi4 frontend test's fake engine already returns owning logits; its
sampler stub now counts samples taken from a buffer that no longer owns
them, and a new test drives the chunked path and requires none.

(cherry picked from commit 23b4fa3296c0812dbddad3051f9922466e142e03)
Co-authored-by: Cursor <cursoragent@cursor.com>
2bf92e8 says it drops three DEBUG prints left over from bringing the
backend up, but its diff removes none: every rai build still printed
"RAI enabled!" and "got device from corelib" on every command, `flm list`
included. The third, "corelib reports no NPU device", only repeated what
npu_open_error already carries to the "rai backend unavailable" line and
the no-device failure below it.

Co-authored-by: Cursor <cursoragent@cursor.com>
flat_mha reads the rotary tables on the host to build the table the kernel
binds; they never reach the device, and corelib rejects a device tensor in
those slots. They were being written as device tensors, which 0.5.0 no longer
accepts.

create_host_view was missing from the resolved symbol table entirely, so add
it, along with a UniqueHostView alias for the lifetime it implies: a host view
borrows the caller's bytes rather than copying them, so RopeTables is now a
member declared ahead of the views that borrow it -- members are destroyed in
reverse declaration order, so the tables outlive the views.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both catch sites discarded the exception and threw a fixed message, so every
backend failure arrived as "Inference failed" with nothing to act on -- the
corelib label naming the failing op was lost at the catch. Route both through
one fail_inference() that appends the backend's own detail.

It also distinguishes the two recovery paths: a poisoned backend needs an
unload/reload, an unpoisoned one only lost the conversation, and the message
now says which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The checks read the NPU's geometry and this process's memlock limit and hold
them against what FastFlowLM's own kernel flow needs. A corelib build does not
use that flow -- corelib owns device setup and brings the NPU up itself -- so
the checks answer a question this build does not ask, against a threshold
describing kernels it does not run. Left in, they fail a working rai install on
hardware corelib runs on perfectly well.

The JSON keeps the shape callers already parse but reports the checks as
skipped rather than claiming they passed. Also drops three DEBUG prints left
over from bringing the backend up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guide covers a from-scratch build: the corelib/DynamicDispatch prefix and
the exact version the headers are pinned to, how the corelib lookup resolves,
the XRT library the installer stages over the real one, the memlock limit and
why a limits file does not apply to an already-running shell, and the known
corelib stream-reuse bug that currently blocks the backend.

home_install.sh's comments named an internal stack identifier that should not
appear in output; use the neutral name instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cb8a19d moved flat_mha's cosine/sine tables from device tensors to host
views and added create_host_view to the resolved symbol table, but the
suite still described the old shape: test_corelib_api counted 26 symbols,
and test_phi4_engine counted 74 device tensors and located writes and the
K/V caches by their old positions. Both failed on the PR head.

The fake corelib now implements create_host_view and, like the real one,
rejects anything but a host view in flat_mha's rotary slots, so a
regression back to device tensors fails the engine test instead of passing
silently. The cache tests find layer 0's K cache by shape rather than by
create index, and validation is checked to create no host views.

Carried from medusa's 4ed7c0b3, test side only; the engine change itself
is cb8a19d's.

Co-authored-by: Cursor <cursoragent@cursor.com>
TestRejectsMissingWrongTypeWrongShapeAndWrongLengthForEveryTensorRole
writes four full-size Phi-4 fixtures for every required tensor role and
keeps up to five of them alive at once. On a shared build box that is
enough to run the volume out of space, and the resulting truncated
fixtures fail as "out-of-file range" rather than as a disk error.

It now runs only with FLM_PHI4_GGUF_EVERY_ROLE set. The rejection paths
it sweeps are still covered one role at a time by the mixed-quantization,
tied-embedding and rope-factor tests that stay in the default run.

Co-authored-by: Cursor <cursoragent@cursor.com>

This branch has not been deployed

No deployments
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.

2 participants