Conversation
chizamd
force-pushed
the
feature/phi4-rai
branch
6 times, most recently
from
September 22, 2026 08:57
a2842c7 to
23c77b1
Compare
chizamd
marked this pull request as ready for review
September 22, 2026 08:57
chizamd
force-pushed
the
feature/phi4-rai
branch
from
September 22, 2026 09:13
23c77b1 to
8362fcd
Compare
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>
chizamd
force-pushed
the
feature/phi4-rai
branch
from
September 22, 2026 09:22
8362fcd to
1313ef8
Compare
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 "!", "%", "…" or " 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Runs Phi-4-mini-instruct from a Q8_0 GGUF, through AMD's
ryzenai-corelib. The weights are read straight from the GGUF — no ONNXmodel, no tensor manifest, no converted weight file is produced or
shipped.
There is no separate tag for it.
phi4-mini-it:4bresolves to theartifacts the build's NPU generation can run: the NPU2/Q4NX entry on
stx, this GGUF entry onaie_next.flm liston a rai build shows onlywhat it can run.
112 files, +12,221 / −1,874 vs
0cfecb08. Packaging and CI are stillabsent — 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/RequireF32take 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.jsonandtokenizer_config.jsonarecross-checked against. A mismatched package fails at load with an exact
diagnostic, never mid-generation.
tokenizer_config.jsonis parsed once by the frontend and passed downthrough
BackendContext, so the model directory layout stays thefrontend'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_CACHEredirects 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_mhaagainstthe 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_RAIpicks which at compile time, andutils::build_npu_platform()is aconstexprthat reports the choice.The catalog in
model_list.jsondeclares which generations each modelsupports and
model_listprunes itself to the one the build was madefor, so
flm listworks on a machine with no NPU without asking thehardware 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_backendisretired, 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 exact0.5.0match isrequired 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, sothe compile-time
#error, the run-time comparison, the fake, the testsand 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.
Phi-4-mini-instruct.Q8_0.ggufunsloth/Phi-4-mini-instruct-GGUF78eb92a4tokenizer.json,tokenizer_config.json,config.jsonmicrosoft/Phi-4-mini-instructcfbefacbAll four are SHA-256 verified before the download is promoted.
Verification is a pull-time concern:
flm pullandflm checkverifyin 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 son both the CLI and server load paths.FLM_RAI_PROFILE_LOAD=1breaks the rai load into shape planning, GGUFresolution, 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,/verboseand theOllama-compatible
eval_durationreport 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()becameget_device(), NULL for no NPU.Three standalone suites, all hardware-free except where noted:
phi4_rai— 9 targets on a fake corelib implementingthe 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_corelibexecuting 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, onstubs. 9/9.
model_list_platform— catalog platform filtering and overridemerging. 6/6.
run_real_rai_acceptance.ps1drives the hardware matrix end to end andwrites a machine-readable record. Its provenance step degrades to
unknownwhen a revision cannot be read rather than failing the run:it used to call
.Trim()on the output of agit rev-parsethat hadprinted 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/chatand/v1/chat/completionsboth 200, streaming andnon-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:
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.mdkeeps the two machines separateand says plainly which figures were measured where, and which were not
re-measured after the packer changed.
the same model answering the same prompt at
temperature=0, top_k=1produced 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.
0.5.0, on an aie_next host:
passed: true, 0 failures, 10/10fresh-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_corelibrunning against the real DLL rather than skipping.calculate_file_sha256is ~8× slower than the work requires —~28 s over 4 GB where
Get-FileHashtakes 3.67 s, because it uses aportable pure-C++ SHA-256. No longer on the startup path, but every
flm pullandflm checkpays it, for every model. Shared pull code,unrelated to this backend, left for its own change.
FLM_RAI_WEIGHT_CACHEdirectory holds one cache, soalternating two models there repacks each time. The default — beside the
model — does not have this.
rai-enabled developer build.
compile the rai path.
Q8_0only — no Q4_0/Q4_K/Q6_K, no mixedquantization, no generic GGUF runtime, no other model family.
the stx entry.
#706 reaches the same backend through an **ONNX manifest
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