Conversation
The AIE4 route has its own decode loop and never touched the DECODING_TIME profiler, so every run reported "Decoding time: 0 us" and an "-nan(ind)" decode speed, and meta_info.decoding_duration stayed zero on all four generation endpoints. Record the profiler around each forward, exactly as the shared decode loop does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drives the full hardware matrix from one script: pinned-package check, the CLI semantic and repeated-load cycles through a PTY, both REST APIs in streaming and non-streaming form, cancellation and recovery, the 4095/4096 boundary, and the descriptive performance record. JSON request bodies are written to files and read back by curl with "@file". Passing a body inline lets PowerShell's native-argument quoting strip the double quotes, which is why the earlier cancellation and boundary probes recorded a server-side JSON parse error instead of a result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A header value containing a space is split into two native arguments, so curl took the second half as another URL and reported "Could not resolve host: application" while sending the request without the intended content type. The colon form carries the same meaning and cannot split. Also give every REST call a timeout and fail loudly when a cancelled stream does not end, so a wedged request reports instead of hanging the run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records the acceptance run on XCOMEDUSAD-43 at 8772108: load to serving 44.2/47.4/49.1 s, 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, with the machine, power scheme, commits, revisions and file hashes it came from. Performance is descriptive; no threshold is claimed. Two numbers are deliberately not presented as throughput. The record's load_duration is 1.5 us, which is the Ollama-compatible field on an already warm server rather than a model load, so load was measured separately over three fresh processes. The ten fresh-process CLI cycles report 3.70-20.26 tok/s because each pays the one-time kernel and ELF setup inside its own eight-token window. The model card separates the two context limits that were previously conflated. The 4096 ceiling is a correctness boundary: 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. The further step down to 4095 is this frontend's own conservatism so an admitted request can always finish, and is not imposed by corelib. Also records the one unexplained reply that degenerated into a markdown image URL, and corrects the emitter comment: the unbounded ConvertTo-Json allocation reproduces on PowerShell 7.0.0 as well, so a newer engine is not a fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Model load is the one phase no profiler covered, and on this backend it is the largest single cost a user waits through. The runner now reports it for every backend, and the AIE4 engine breaks its own load into shape planning, GGUF resolution, host preparation, weight requantization and device allocation, printed when FLM_AIE4_PROFILE_LOAD is set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The helper was local to the runner, so `flm serve` -- the path that actually waits 45 s on this backend -- reported nothing. Move it to debug_utils and call it from ensure_model_loaded, which is the single funnel for the server's initial load and every later model switch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Engine load is only 16 s of it. The startup SHA-256 check over the 4 GB GGUF is ~28 s, 62% of the wait, and happens before the engine is constructed. Weight requantization is 15 s. Everything else is under a quarter of a second. The integrity check is also ~8x slower than the work needs: Get-FileHash over the same file on the same machine takes 3.67 s against ~28 s for calculate_file_sha256, which uses a portable pure-C++ SHA-256 with no hardware acceleration. That cost is paid by every model on every startup and pull, not just this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ead hint Two independent costs in the ~45 s startup, measured not assumed. The larger one is the startup integrity check: is_model_downloaded re-hashed every pinned file on every launch, ~28 s of SHA-256 over the 4 GB GGUF, 62% of the wait. That is a pull-time concern; the parameter to skip it already existed and simply was not passed. The run and serve paths now ask for status only. `flm pull` and `flm check` are unchanged and still verify in full, so a corrupt file is still caught -- at the next explicit check rather than at every launch. The smaller one is the packer. corelib treats a threads hint of 0 as ONE, deliberately, and this requantizing path is compute-bound and scales with it. The hint is per-create and the creates stay serialized. corelib's header records that loading a model with 8 CONCURRENT creates on this entry point failed 2 of 10 with all-zero output -- token id 0 at every step -- against 0 of 10 serialized and 0 of 10 for the ONNX path, with attribution open and the leading hypothesis an incomplete host-to-device sync. That is the faster configuration and it is deliberately not taken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chizamd
force-pushed
the
feature/phi4-gguf-aie4
branch
from
September 12, 2026 09:20
08277ef to
95c5c82
Compare
Re-measured after removing the per-launch re-hash and giving the packer a thread hint: process launch to serving 5.1/5.3 s against 44.9/46.3 s, with requantization at 2.5/3.0 s against 15.3/14.9 s. Output re-verified after the packing change; no degeneration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chizamd
marked this pull request as ready for review
September 14, 2026 23:26
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.
Summary
Adds one catalog model,
phi4-mini-it-aie4:4b, that FastFlowLM pulls as a Q8_0 GGUF and runs on AIE4 through AMD'sryzenai_corelib.dll. The weights are read straight from the GGUF — this branch does not generate, ship, or consume an ONNX model, an initializer manifest, or any converted weight artifact.The existing Phi-4 NPU2/Q4NX model and its backend are untouched. The new path compiles only under
-DFLM_ENABLE_CORELIB_AIE4=ON(Windows + XRT); with the option OFF,common/corelib/*.cppis filtered out of theflmsource glob and the binary contains no reference to it.52 files, +7,738 / −385 vs
2d6c4838.Draft because it is an alternative to #706 rather than an increment on it, and because packaging and CI are absent.
Design
GGUF is the only source of truth, read zero-copy
The file is memory-mapped and every tensor is handed out as a
span— nothing is 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 is created. Head counts, rope base and the short-factor table come from the file's own metadata; the architecture itself (32 layers, 3072 hidden, 24/8 heads, 200064 vocab) is a fixed C++ constant block that the GGUF metadata,config.jsonandtokenizer_config.jsonare all cross-checked against. A mismatched package fails at load with an exact diagnostic, never mid-generation.Weights go straight from mapped bytes into corelib objects
Each projection calls
matmul_weights_create_gguf_requantizedon the Q8_0 bytes in place — no intermediate blob, no packed-weight file. Q8_0 is lossily requantized to group 64 during packing. The MLP is one fused SSMLP object per layer that absorbs the FFN norm scale, epsilon, and the next layer's attention norm scale, so the only host RMSNorm in the model is layer 0's. The LM head is built fromtoken_embd.weight(tied embeddings), requantized the same way.The packer is given a threads hint of 8 — corelib treats 0 as ONE deliberately, and this requantizing path is compute-bound and scales with the hint. The hint is per-create and the creates themselves are serialized. corelib's header records that loading a model with 8 concurrent creates on this entry point failed 2 of 10 with all-zero output, against 0 of 10 serialized, with attribution open; that configuration is deliberately not used, and a test pins one in-flight create.
One code path for prefill and decode, and it never allocates
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_mhaagainst the caches with the rope tables, O projection, fused SSMLP with a residual/skip ping-pong. The result is two synchronizes per token — one after all 32 layers, one after the head — not one per layer.Explicit routing, no fallback, terminal failure
ResolveBackend()dispatches onmodel_info.details.execution_backend(corelib_aie4_gguf). Nothing is inferred from hardware, filename, or quantization level, and there is no fallback to CPU or NPU2 — if corelib is missing or the wrong version the model fails to load with a diagnostic.flm.exenever linksryzenai_corelib.lib; the DLL is resolved by absolute path (FLM_AIE4_CORELIB_PATH, else<exe_dir>/aie4/ryzenai_corelib.dll) andget_versionis called first, so a mismatched runtime reports a version error rather than a missing symbol. An exact0.3.0match is required while corelib is pre-1.0.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.
Pinned multi-source pull
Phi-4-mini-instruct.Q8_0.ggufunsloth/Phi-4-mini-instruct-GGUF78eb92a4tokenizer.json,tokenizer_config.json,config.jsonmicrosoft/Phi-4-mini-instructcfbefacbmodel_list.jsongains afile_sourcesmap for per-file source/revision overrides. Existing catalog entries are not migrated — entries withoutfile_sourceskeep their current behaviour, andtest_model_downloader.cpppins that compatibility. All four files are SHA-256 verified againstmodel_info.jsonat pull time and onflm check. The run and serve paths check presence and version only; they do not re-hash a 4 GB file on every launch.Capacity
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.Observability
Model loaded in N sis reported on both the CLI and server load paths.FLM_AIE4_PROFILE_LOAD=1additionally breaks the AIE4 load into shape planning, GGUF resolution, host preparation, weight requantization and device allocation. Decode timing is recorded on the AIE4 path, so/status,/verboseand the Ollama-compatibleeval_durationreport real figures on this backend.Tests
src/test/phi4_corelib_aie4/is a standalone CMake project with 9 ctest targets built on a fake corelib implementing the 0.3.0 C ABI in-process, plus a deterministic GGUF v3 fixture builder that synthesizes valid and deliberately corrupt packages. Coverage: ABI/version/path/lifetime, parsing and corruption, host ops, shape plan, engine load and dispatch sequencing with object-leak and serialization checks, frontend routing and request lifecycle, a compile-gate proving the OFF build carries no corelib, and the downloader including legacy-entry compatibility.9/9 pass on the AIE4 host with
test_real_corelibexecuting against the real DLL, not skipped.src/test/phi4_corelib_aie4/run_real_aie4_acceptance.ps1drives the hardware matrix end to end and writes a machine-readable record.Verified on hardware
Machine
XCOMEDUSAD-43, AMD XDNA NPU, Windows 11 build 26100, corelib3c35aebdABI 0.3.0. Acceptance recordpassed: true, 0 failures:pull/check— four pinned files SHA-256 verified; the model directory contains exactly those four, with no manifest/onnx/converted/packed artifact.Backend: corelib_aie4_ggufand the loaded DLL path reported in every one./api/chatand/v1/chat/completionsboth 200, streaming and non-streaming.Measured
Descriptive, no threshold claimed.
Ten fresh-process CLI cycles report 3.70–20.26 tok/s decode; those are not throughput, because each pays the one-time setup inside its own eight-token window. The 1.7× spread between the REST and warm-CLI decode figures is unexplained; treat single-run differences below roughly 2× as noise.
Known gaps and defects
/api/chatanswer toWhat is 2+2?came back as a truncated markdown image URL withdone_reason: length. The same prompt answered correctly on every other occasion, so it reads as sampling nondeterminism rather than a routing fault — but it is a single observation and it is not understood.87721089. Later commits changed startup verification and the packer thread hint. After those, the hardware suite (9/9 with the real DLL) and output correctness were re-verified and load was re-measured, but the full matrix has not been re-run at the tip.calculate_file_sha256is ~8× slower than the work requires — ~28 s over 4 GB whereGet-FileHashon the same machine takes 3.67 s, because it uses a portable pure-C++ SHA-256 with no hardware acceleration. This no longer sits on the startup path, but everyflm pullandflm checkstill pays it, for every model. Not fixed here: it is shared pull code, unrelated to AIE4, and belongs in its own change.Q8_0only — no Q4_0/Q4_K/Q6_K, no mixed quantization, no generic GGUF runtime, no other model family.Relationship to #706
#706 (
feature/phi4-aie4-corelib) reaches the same backend through an ONNX manifest + packed weights pipeline, and carries packaging and tooling this branch does not. This is the independent GGUF-direct approach: far smaller surface, no manifest generator, no converted artifacts, no Python at runtime. They are alternatives, not increments; only one should land.