Skip to content

feat(phi4): optional AIE4 corelib backend for Phi-4-mini-instruct - #706

Draft
chizamd wants to merge 117 commits into
ROCm:mainfrom
chizamd:feature/phi4-aie4-corelib
Draft

feat(phi4): optional AIE4 corelib backend for Phi-4-mini-instruct#706
chizamd wants to merge 117 commits into
ROCm:mainfrom
chizamd:feature/phi4-aie4-corelib

Conversation

@chizamd

@chizamd chizamd commented Sep 3, 2026

Copy link
Copy Markdown

Summary

This branch adds a second, optional execution backend for Phi-4-mini-instruct: corelib_aie4, which runs the model on AIE4 through AMD's ryzenai_corelib.dll instead of the existing XDNA2/Q4NX path. The existing backend is untouched; the new one is compiled only when -DFLM_ENABLE_CORELIB_AIE4=ON (Windows + XRT), and with the option OFF the build contains no reference to common/corelib at all.

New model tag: phi4-mini-it-aie4:4b, pinned to amd/phi-4-mini-instruct-oga-dml @ HF e751fb68.

273 files, +73,416 / −289 vs 2d6c4838. Roughly a third of that is production code; the rest is tests, tooling and committed evidence.


Design

1. Loading, never linking

flm.exe never links ryzenai_corelib.lib. src/common/corelib/corelib_api.cpp resolves the DLL by absolute path (RYZENAI_CORELIB_PATH, else <exe_dir>/aie4/ryzenai_corelib.dll), calls get_version first so a mismatched runtime reports a version error rather than a missing symbol, and requires an exact major.minor.patch match while the compiled major is 0. All ~25 entry points land in a CorelibFunctions struct. Tensor I/O is element-typed (WriteElements/ReadElements, never byte-taking) so the FP32↔BF16 2× hazard cannot be expressed. An RAII object wrapper counts creations per kind, which is what makes "no allocation after warmup" a measured property rather than an assertion.

corelib_runtime.cpp is a process singleton: it prepares the fatal-record store, runs selftest_dependencies, requires a device context, and owns AcquireExecution() — a process-wide mutex that is the single serialization point for all corelib work.

2. Failure policy

Some corelib failures are irrecoverable (work is in flight on the device); most are not. corelib_fatal_record.cpp pre-creates a pending-corelib-fatal-*.tmp file under LocalAppData at startup, so a crash record can be written without allocating at failure time, then renamed to corelib-fatal-*.json with status/call/phase/layer/rows/position. Records left by previous processes are drained and reported on every startup (main.cpp).

The engine draws the irrevocable boundary per submission group, not per step: checked_submit marks state irrevocable only after Check succeeds (a rejected dispatch enqueued nothing), and checked_synchronize clears it after a successful barrier. Net effect: a host-side failure after a completed synchronize throws CorelibError, the frontend clears the conversation and the server answers 500 — only a failed synchronize terminates the process, and it does so with a persisted record and exit code 0xE0040001.

3. Model package layers

  • Manifest (phi4_corelib_manifest.cpp) — loads corelib_phi4_manifest.json, memory-maps model.onnx.data, exposes ONNX initializers as views plus the MatMulNBits (qweight/scales/qzeros) and fused SSMLP object descriptions.
  • Weights (phi4_corelib_weights.cpp) — packs each object straight from the mapped ONNX bytes via matmul_bf16_weights_create_onnx / ssmlp_bf16_weights_create_onnx; 32 layers × 5 objects + LM head.
  • Shape plan (phi4_corelib_shape_plan.cpp) — at load time it interrogates corelib's own padding helpers for live rows 1..4096 and records the padded-row transition tables per use (Q/KV proj, attention, O proj, SSMLP, LM head). Runtime lookups never call the helper again. This is honest but expensive: it is ~86% of the 12.8 s model load, and it is the obvious thing to attack next.
  • Host ops (phi4_corelib_host.cpp) — embedding gather + FP16→FP32 widening, RMSNorm, K/V cache scatter, argmax.

4. Engine and frontend

phi4_corelib_aie4 : causal_lm implements the same interface as phi4_npu, so the swap is invisible above it. One RunRows() path serves both prefill and decode: per layer, Q/K/V matmul → synchronize → host V scatter → flat_mha → O proj → SSMLP, then LM head.

ResolveBackend() in modeling_phi4.cpp dispatches on model_info.details.execution_backend. The AIE4 branch requires the tokenizer/config files, rejects preemption, and hard-fails if the shipped overlay config.json disagrees with flm::phi4::constants — the overlay restates the contract, it never redefines it.

Continuation routing. On a prefix hit the frontend chooses between replaying the suffix token-by-token and re-prefilling the whole history. The crossover was measured, not guessed: at a 2048-token history, appending 1 token is 54.2 ms vs 1150.1 ms for re-prefill (21×), while appending 256 tokens is 10.4 s vs 1.15 s. The constant is append when suffix ≤ 4 tokens, else re-prefill, and the block in phi4_corelib_aie4_tuning.hpp is generated by tools/calibrate_phi4_corelib_continuation.py carrying the measuring host, corelib SHA-256 and model SHA-256.

Capacity. The active cap is one below the 4096 attention window, so any admitted request can always finish; over-cap requests are rejected with HTTP 400.

5. Server, CLI, packaging

  • src/server/generation_limit.cpp centralizes the four generation endpoints: limit parsing (max_tokens / options.num_predict / max_completion_tokens), per-backend loop limits, and mapping of Ollama's non-positive "generate forever" sentinels (-1, -2, 0) to unbounded instead of a 400.
  • npu_access_manager.cpp adds a process-wide exclusive NPU lock with queueing for the six device-using routes.
  • flm validate gained per-backend sub-reports (loader / dependencies / device context / fatal log / shutdown / ready); shutdown is ordered so corelib teardown happens after engine destruction on every command path.
  • src/pull/model_overlay.cpp copies the four shipped overlay files into the pulled model directory after SHA-256 verification, with path-traversal and record-shape validation.
  • cmake/StageAie4Runtime.cmake derives the DLL closure with file(GET_RUNTIME_DEPENDENCIES) against the actually shipped ryzenai_corelib.dll (plus dyn_bins.dll, which is loaded by name and invisible to a walker), hard-fails on unresolved or conflicting DLLs, and emits a shippable aie4-closure.txt with hashes. ReadCorelibVersion.ps1 then loads the staged closure and calls get_version — a closure that cannot load does not ship. MSI (Aie4Feature) and Inno (aie4runtime task) both carry the runtime as an optional component.
  • tools/ adds manifest generation, model packaging with separated provenance, continuation calibration, baseline reporting, and an FP64 host LM-head reference used to exonerate the LM head in the determinism investigation.

What state this is in

Verified: ctest 17 passed / 0 failed (4 skipped: hardware-only); python -m unittest discover -s tools/tests 312 OK; verify_acceptance_guards.ps1 exit 0 with 72 assertions on both PowerShell 5.1 and pwsh 7. The acceptance document re-renders byte-for-byte (80,544 bytes) from its committed JSON.

Real-hardware acceptance (docs/docs/benchmarks/phi4_aie4_acceptance.md, machine XCOMEDUSAD-43, XDNA driver 32.0.20214.4161): 16 steps met, 1 not met, 3 not exercised; design criteria 12 met, 12 partial, 2 not met, 26 not exercised. "Not exercised" is recorded as not evidence, not as a soft pass. The package/contract checks, the "no corelib import in flm.exe" check, the version gate, the 6-DLL closure, conversation isolation, the 4095 boundary and the terminal-failure diagnostic all passed. The MSI + model package was separately smoke-tested by hand on a second machine that took no part in development.

Measured (docs/docs/models/phi.md): load 12.8 s; warm TTFT 53.5 ms; decode 22–25 tok/s at 128/512/2048 context; prefill 2,342 tok/s at 4096 rows (engine-level); peak host private bytes 7.68 GiB; zero device-tensor and zero net-object growth over a warm 128-token window. Note that decode throughput varies up to 1.8× across machines with no code change — sub-2× differences should be treated as unresolved.

Known defects, disclosed rather than smoothed over

  1. Run-to-run nondeterminism produces different text, not just different last bits. 3 of 150 committed run-pair records diverge in emitted tokens; max absolute logit difference 48–49, ~196k of 200,064 logits differing at the divergence step, and the 2-BF16-ULP bound the suite gates on was breached. The LM head is exonerated against an FP64 host reference, so the search is for an unpinned accumulation or work-partitioning order inside the 32-layer body. Cause unknown, uninvestigated. Records under src/test/phi4_corelib_aie4/determinism_records/.
  2. Long-generation collapse. Two acceptance turns ran to the cap without a stop token (~187 s); one repeated a phrase 375×, the other degenerated into high-entropy ASCII at ~85% of the run. Single observation, not investigated. Mitigation is an explicit generation limit (/set gen-lim, or max_tokens / options.num_predict over HTTP); there is no CLI startup flag for it.
  3. Multi-turn history is lost in flm run (acceptance Step 8 = not met). This is a pre-existing FastFlow frontend defect, identical in the Llama3/Qwen/Gemma frontends and located above the AIE4 branch. Workaround: /api/chat with a full messages array.
  4. Usable context is 4095, largest admissible prompt 4,094 — one below the design's 4096, by construction (see §4).
  5. No CI runs any of this, and the release preset does not compile the AIE4 path. Recorded as a known gap.
  6. The MSI is not fully self-contained: a Boost linkage regression introduced by this branch was found and fixed, but five FFmpeg imports inherited from building against a conda prefix remain, and with conda stripped from PATH flm.exe exits 0xC0000135. Pre-existing, now measured by tools/tests/test_windows_dependency_linkage.py.

Corrections to the acceptance record are published on a separate provenance page (phi4_aie4_acceptance_provenance.md) rather than edited into the generated document, so the record and its corrections can both be audited.

Review notes

Every task went through implement → review → fix → re-review, followed by a whole-branch review, a combined fix round, a scoped re-review, and a residual round. The remaining open items are listed above; none of them are silent.

chizamd and others added 30 commits August 31, 2026 20:34
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The upstream C ABI changed in ways that a rename sweep alone would not
have caught, so this lands the whole adapter boundary at once: nothing
between the version gate and the element-unit conversion compiles in
isolation.

- Gate the load on ryzenai_corelib_get_version, resolved and checked
  before any other symbol. While corelib is pre-1.0 all three version
  components must match exactly, because the header says the API may
  change in any release. Resolving the rest first would have reported a
  missing renamed symbol instead of the skew that caused it.
- tensor_write / tensor_read now count ELEMENTS of the tensor's own
  dtype. Every call site moves to CorelibApi::WriteElements /
  ReadElements, and no byte-taking spelling remains: the two counts
  differ by 2x when FP32 crosses into a BF16 tensor, so the wrong one
  half-fills or overruns rather than failing.
- ryzenai_corelib_convert and _convert_strided are gone. The RoPE slice
  becomes a bounds-checked host gather in the source dtype, and the
  activation path stays in FP32 and lets tensor_write narrow, which
  leaves exactly one BF16 rounding implementation on that path. The two
  host conversions design API-6 still permits move into
  corelib/host_convert.hpp.
- An FP32 scales array is now rejected with an actionable error rather
  than narrowed, since narrowing it would need a third host converter.
- The fake corelib exports the e5258d2 symbol set and models tensors
  with a real dtype and element count, so a byte-sized count is
  rejected. A fake that accepted both conventions would have let this
  whole change pass while moving twice the data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every other test in this suite runs against fake_ryzenai_corelib, which is
FastFlow's own code. That validates FastFlow against FastFlow's model of
corelib rather than against corelib -- and the model stayed green through
a full ABI break, which is the gap this closes without needing hardware.

The header documents three things as NPU-free, and those are what runs:
get_version and selftest_dependencies; the three shape helpers; and
has_device_context, which is recorded rather than asserted because the dev
box has an NPU but is not the AIE4 target.

The shape check is the load-bearing one. Against the real e5258d2 library
the padded K/N equals the logical K/N for all three Phi-4 MatMul
descriptors, and every helper leaves rows = 1 at 1 -- so MEM-5 and the
Task 5 capacity model hold against the shipped kernel set, not just
against a fake that returned what FastFlow expected.

It also found that the fake did: identity padding, where the real grid is
{1, 64, 128, 256, 512, 1024, 2048, 3072, 4096}, and an LM head that ships
only 1 and 128 and REJECTS anything larger rather than rounding up. The
fake now reproduces both, and the real test asserts the two agree, so a
future kernel-set change surfaces here instead of on hardware.

Configured-but-missing is a failure rather than a skip: a silently skipped
ABI check is how a stale DLL stays hidden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 10 wired ParseGenerationLimit into handle_generate,
handle_openai_chat_completion and handle_openai_completion. It missed
handle_chat, which serves /api/chat and computed its limit through a
separate path, OllamaChatGenerationLoopLimit.

/api/chat is the Ollama-compatible endpoint and so likely the most used
one. On the AIE4 tag it was bypassing the explicit-limit detection, the
prompt_tokens + requested_max_new_tokens admission rule, the HTTP 400 path
and session_cleared reporting -- silently capping at the legacy 4096
instead of running to the context cap, and never checking that an
explicit num_predict fits.

ParseGenerationLimit gains an OllamaChat case reading options.num_predict,
and OllamaChatGenerationLoopLimit is now defined in terms of it, so the
legacy and shared rules cannot drift. Legacy behaviour is unchanged: an
omitted num_predict is still 4096 and an explicit one is still honoured.

Three-of-four was found by inspection, so this also adds the rule.
GenerationRoutes() declares every route that reaches the causal engine,
each handler resolves its endpoint through it, and a test derives the set
independently -- reading rest_handler.cpp for handlers that call
generate()/generate_with_prompt(), mapping them to their server.cpp
registrations, and failing if any is undeclared. A new generation endpoint
now cannot reach the engine without declaring how its limit is parsed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 1. The code the guards protect was right; the guards were
not. Each of these was verified by mutation, not by inspection.

test_real_corelib returned 0 when no runtime directory was configured, so
CTest reported Passed. The suite's highest-value check was green and inert
by default, which reads as coverage it does not have. It now returns
CTest's SKIP_RETURN_CODE and reports Skipped. Configured-but-missing stays
a hard failure.

The corrected pad grid was consumed only by that skippable test -- every
other host test overrides the pad helpers -- and the LM-head refusal
branch had no caller at all. test_phi4_shape_plan now builds a plan
against the unmodified fake and asserts the grid, the capacities and the
refusal directly. The two tests form a chain: test_real_corelib asserts
fake == library, this asserts fake == the numbers written down, so a
regression at either end fails something that runs without a runtime
directory.

The real library's refusal above M = 128 on the LM head is now asserted
rather than avoided. That refusal is what makes Phi4ShapePlan correct in
querying the LM head at one row and having RowsFor throw above it; if the
library ever started rounding out-of-grid M up instead, the design would
be resting on a property it no longer had.

The route-coverage rule caught an undeclared fifth endpoint but not a
declared one computing its limit privately -- which is exactly the shape
of the /api/chat defect it was written for. handle_chat was declared and
routed all along. Each generating handler must now be shown to call
ParseGenerationLimit, GenerationLoopLimit, RequestedMaxNewTokens and
RequireGenerationEndpoint, to call no other *GenerationLoopLimit, and to
declare the route it is actually registered on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The staged closure is now enumerated by a dependency walker run against the
exact ryzenai_corelib.dll being shipped, per design CLOSURE-1. The previous
hardcoded list was wrong in both directions on this box: it demanded
xrt_core.dll and xrt_umddml.dll, which do not exist here and which the real
DLL does not import, and it named zlib1.dll while conda's libprotobuf.dll
actually imports zlib.dll.

RYZENAI_CORELIB_RUNTIME_DIR is now required only by the install/package step.
flm.exe resolves the corelib DLL at run time by absolute path and never links
ryzenai_corelib.lib, so a feature-ON configure warns rather than failing.

The packaging test now proves the closure per CLOSURE-2 with a negative
control: each derived DLL is hidden in turn and the load must fail. Without
that, a green result only shows the machine had the DLL somewhere, which is
exactly the failure mode that produced Win32 error 126 on a clean box.

Both get_files.bat scripts no longer abort when no AIE4 closure is staged.
Requiring one made the optional feature a precondition of shipping the
ordinary NPU2 installer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The overlay config.json restated the Section 5.1 constants but nothing failed
the load when it disagreed with them, so a hand-edited or stale overlay could
silently redefine layer count, head geometry or vocabulary size for a model
whose weights say otherwise.

The AIE4 load path now compares every declared value against
flm::phi4::constants, which is the same set the manifest loader validates the
ONNX initializers against. A disagreement is a hard load failure in either
direction: the overlay restates the contract and is never the source of truth.

The test corrupts each field in turn and requires the load to throw before the
engine is constructed. Verified RED by neutering the check, which fails the
test, and GREEN with it restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 4's rule as written could not hold. Requiring every model_list.json file
to have a matching Hugging Face record fails by construction: config.json,
corelib_phi4_manifest.json and provenance.json are FastFlow-authored and do not
exist upstream. Per PACKAGE-1 the two sets are now validated apart.

Upstream files must each carry a metadata record at the pinned revision.
Authored overlays must exist in the installed overlay directory and must have
NO upstream record; acquiring one means FastFlow's contract was published to
the model repository. tokenizer_config.json is the documented exception: it
exists upstream and the overlay shadows it, because the published file carries
neither a chat template nor eos_token_id.

Three catalog corrections fall out of reading the spec against the entry:

- genai_config.json was being downloaded. MODEL-2 excludes it, because
  flm.exe runs no ORT or genai graph and an unused config invites a future
  reader to treat it as authoritative.
- .gitattributes was being downloaded. It is a Git repository artifact.
- provenance.json was installed but never staged into the model directory, so
  the record that makes the two provenances checkable was missing from the
  place that needs it. It is now the fourth bundled overlay.

Size and footprint now state what they cover -- the assembled on-disk
directory -- and both the tool and the C++ test derive them instead of
restating a literal.

CMakePresets FLM_VERSION moves to 1.0.4. The overlay declares flm_version
1.0.4 and the catalog demands flm_min_version 1.0.4, so a 1.0.3 binary reports
Incompatible for its own catalog entry. A new test pins that three-way
relationship; it fails when built as 1.0.3, which is how it was verified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 8 checks dumpbin /DEPENDENTS on a packaged flm.exe, which only runs where
an exe exists. The packaging test now also rejects any build file that links
ryzenai_corelib, catching the regression at its source rather than at the end
of a release build. Verified by adding such a link, which fails the test.

Step 2 forbids hand-estimating the catalog numbers. The entry is computed from
upstream metadata because nobody assembles a 3 GiB directory to write a catalog
line, so a new test measures a stand-in assembled directory with
catalog_measurements and requires the same size and footprint. If the two
derivations drift, the published number stops describing the directory a user
actually gets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 1. Four Important findings shared one theme: guards that do not
fire, and a script reporting success for work it never did.

I1. The no-corelib-import guard matched line by line, so it missed the
multi-line target_link_libraries() form -- the only form the AIE4 target uses.
With Step 8's dumpbin unrunnable here, that guard is the sole cover for a
binding constraint, and it was inert against the exact regression it names. It
now parses balanced-paren command bodies across every CMake file in the tree
(48 link calls scanned) and fails if the scan matches nothing. Verified by
linking ryzenai_corelib on its own line: previously passed, now fails.

I2. A multi-entry FLM_AIE4_DEPENDENCY_DIRS was interpolated unescaped into the
POST_BUILD command, so "a;b" became two argv entries and everything after the
first directory was silently dropped -- then staging failed telling the
developer to add a directory they had already added. Reproduced, fixed by
escaping, and re-verified with the needed DLLs reachable only via the second
entry. The ctest registration passes the list through, so CI exercises it.

I3. test_packaged_runtime.ps1 was not registered anywhere. Every guard in it
ran only when someone remembered the command. It is now a ctest test with
SKIP_RETURN_CODE 77 and inherits the configured runtime directories.

I4. The script printed a success line when the real-closure and flm.exe blocks
were skipped. That is the defect Task 10R had to fix in test_real_corelib. It
now enumerates RAN and SKIPPED blocks and exits 77 when anything was skipped,
so a partial run shows as Skipped rather than Passed.

M-esc-1. aie4-closure.txt shipped absolute build-machine paths into bin/aie4.
The shipped report now lists names and SHA-256 only, which is what a recipient
can act on; the path-bearing audit record stays in the build tree. The test
rejects any absolute path in the shipped report and verifies every recorded
hash against the staged file.

M-esc-2. The docs called all four overlay files authored. Three are authored
with no upstream counterpart; tokenizer_config.json shadows a published file
and legitimately has an upstream record. Blurring that is what caused the bug.

Also, per the coordinator's note: AutoModel overwrites the tokenizer_config
chat_template with a standalone chat_template.jinja when present, so the
inlined copy is dead unless byte-identical. They are identical today (423
bytes, sha256 febf5892...) but nothing asserted it. Generation and offline
validation now require the equality, and overlay generation reads the template
as bytes so newline translation cannot break it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chizamd and others added 26 commits September 2, 2026 21:25
Step 8 sat for thirty-six minutes on a turn that had finished normally.

The driver decides a turn is over by counting `>>>` prompts at the start
of a screen row. When a reply ends without a newline -- which is what
happens when it contains characters the console does not render one cell
per code unit, emoji here -- the REPL writes its prompt at whatever
column the cursor stopped at, and the row arrives as a run of spaces
followed by `>>>`. Anchored hard at column zero, the matcher saw no new
prompt and waited out the full turn timeout against a REPL that was
sitting there ready.

Allowing leading whitespace fixes it. That is the second time this one
regex has cost a run: the first was requiring a trailing space that
Read-Screen's TrimEnd had already removed. Both failures look identical
from outside -- a driver waiting on nothing -- and both are now written
down next to the pattern, because the next person to touch it will be
tempted to tighten it again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One uninterrupted `-Steps all` run at 0b011f2 against an flm.exe built
on the target from f67ed00, with no product source differing between
them and both script hashes matching the files as checked out. Nothing
carried forward.

Sixteen steps met, one not met, three not exercised. Twelve of
fifty-two Section 17 criteria met, twelve PARTIAL, two not met,
twenty-six not exercised.

The met count fell from twenty-three to twelve, and that is the review
working rather than a regression. Eleven of those criteria had been
rounded up from steps that covered part of what they say. C20 is the one
worth reading: the admission rule is verified exactly, and against 4095,
while the criterion says 4096.

Three steps changed status on their own merits. 9c is not exercised
because its measurement came out inconclusive and saying so beats
picking the nearer prediction. 8c is now met, on data the record had
been holding while claiming not to have it. 3b and 6b are new and met,
running the binary that steps 3 and 6 used to cite.

The one product failure is unchanged and is the CLI retaining no
conversation history. Both NOT MET criteria, C21 and C38, trace to it,
and the model page now says it is not specific to this model or this
backend -- every text frontend builds the same single-message prompt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last run exited 1 with its real verdict, wrote a complete JSON
record -- and left the PREVIOUS document on disk. The committed
document went on stating "each recalled only its own secret" for Step
9b, the exact claim the record had already retracted, because the
renderer crashed before overwriting it. That is the failure this
project has hit four times: the fix reaches the code and the report and
not the rendered artifact. Getting it again inside the change that was
supposed to close it is not a coincidence worth ignoring.

The crash: Step 8c stored its rows under `per_turn`, the same field
Step 8 uses for conversational turns, and the verbatim renderer keys off
that name and reads `.index`. Renamed to `positions_per_turn`, and the
renderer now reads every field through Get-Field so a shape it does not
expect degrades instead of throwing.

The silence is the more important half, and is fixed separately from the
crash that exposed it. The document is deleted before it is written,
carries the run stamp, and is read back and checked for that stamp; a
render failure is caught, recorded in the JSON as
`document_render_error`, and forces exit 1 with its own RESULT line.
Absence is now the failure mode instead of staleness, and no future
renderer bug can leave a stale document passing for a fresh one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the pair committed in fe4bf04, where the JSON was fresh and
the document was the previous run's -- still publishing the Step 9b
sentence the record had retracted.

Both artifacts now carry run-stamp 20260903T051041Z and it is checked
rather than assumed: the harness writes the document, reads it back, and
fails the run if its own stamp is not there. Verified after the fact
too, against the committed files, along with the two script hashes
matching the checked-out sources, binary_revision f67ed00 against
checkout 81a01f7 with no product source differing, and nothing carried.

Sixteen steps met, one not met, three not exercised. Twelve criteria
met, twelve PARTIAL, two not met, twenty-six not exercised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 8's history-accumulation gate required /history's token count to
grow across turns. Those counts are the length of the CURRENT turn's
prompt plus reply, so they move for reasons that have nothing to do with
whether the previous turn was kept.

It fails both ways. An earlier run of this step recorded [76, 81, 94,
29] -- three consecutive increases, on a conversation that demonstrably
was not accumulating -- and the gate fired only because turn 4's reply
happened to be short. Had it run long, the harness would have concluded
the history WAS accumulating on the run that disproves it. And a
genuinely accumulating conversation that saturates the cap produces
equal successive counts, which "must increase" reads as a defect.

The gate now asserts containment: turn N's /history must contain turn
N-1's message. Same evidence, already stored, better question. Whitespace
is deleted from both sides rather than normalised, because /history is
read back from the console screen buffer and a wrap splits a word with
no separator -- collapsing runs of spaces would leave "remem ber" and
fail a history that is perfectly correct. The token sequence is still
recorded and is now explicitly marked observation, not verdict.

No hardware run is authorized this round, so both new guards are
verified offline instead, by verify_acceptance_guards.ps1 against the
committed record: containment fires on turns 2, 3 and 4 of the recorded
defect, fires on the near-miss the old gate passed, and stays silent on
a synthetic accumulating conversation wrapped mid-word.

Also here.

The document readback proved freshness but not content. Get-Field
returns $null where direct access used to throw, so a per_turn row of
unexpected shape would render blank turn blocks, pass the run-stamp
check and report success -- staleness closed, fresh-but-empty opened.
The prompts are now collected as they are rendered and checked by
literal containment against the written file, with a count check per
step. Not by parsing '>' lines back out of the markdown: a reply may
contain one, and a check that cannot tell a prompt from a quoted line
will eventually pass on nothing. Verified the same way, against blank
prompts, missing text and a dropped turn.

Two criteria claimed `met` with an undeclared coverage gap, which is the
failure the gap mechanism exists to prevent, one level up. C31 says the
version is checked BEFORE any other symbol; step 3b measures only that
the versions match, and the ordering is read from CorelibApi::Load
rather than observed. C01 says the tag selects ONLY corelib_aie4; one
positive engine-name observation cannot establish a negative. Both now
declare it and will render PARTIAL.

9c recorded a superlinear concurrency cost and named it nowhere: 6.462 s
of wall clock for 4.036 s of sequential work, 1.60x the serialized
prediction. Overlapping requests costing more than serializing them is a
plausible signal on a single mutable KV session. It now emits
concurrency_costs_more_than_serial, an overhead ratio and a finding
string into the artifact. Still not attributed -- guessing is what the
previous version of that step did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page warned that an unbounded request runs to the context cap and
cited "182 seconds and a repeating phrase" -- a figure from an ad-hoc
session that was never committed, and an understatement of what the
acceptance run recorded.

The committed record has two turns reaching the 4095-token cap, at
186.9 s and 179.9 s. One repeated a single phrase for the remainder. The
other degenerated into meaningless characters, and that transcript is
published verbatim in the acceptance document, so a user can find it
whether or not this page mentions it.

Latency was the wrong thing to lead with. A user who reads "may take a
while" sets no limit and waits; a user who reads "three minutes of
noise" sets one. Now it says the output falls apart, with the measured
numbers, and tells them what to do about it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The render guard tested for per-turn evidence with
`@(Get-Field ...).Count -gt 0`. In PowerShell `@($null).Count` is 1, so
it selected all twenty steps and threw on the first one without turns. A
run in which every step passed would have ended in DOCUMENT NOT RENDERED
and exit 1, after ninety minutes of hardware, with a
`document_render_error` that was not about the document.

The verifier could not have caught it, and that is the part worth
fixing. It did not import the harness -- it RE-IMPLEMENTED the check and
hand-fed it `@([pscustomobject]@{id='8';count=...})`, which is precisely
the derivation the bug lived in. A copy of the implementation agrees
with the implementation by construction. It passed; the thing it stood
for did not work.

So the logic now lives once, in acceptance_guards.ps1, dot-sourced by
the harness and by the verifier. The verifier calls the same functions
on the real committed record, loaded the way the harness loads it, and
derives what it used to be handed -- Get-StepsWithTurns is called, not
supplied. It found its first bug within a minute of being written: an
empty array returned from a PowerShell function unrolls to $null, so
`return @()` made `.Count` throw under StrictMode.

The fix for THAT exposed a third shape. `return , @()` survives
assignment but not `@(f ...)`, which keeps the wrapper and reports 1 for
a four-element result. Three behaviours, any two of which look
consistent. Rather than pick a better array convention, counting stops
crossing the boundary as an array: Get-FieldCount returns an [int], and
nothing unrolls or wraps a scalar. Every presence test goes through it.

Also here.

The renderer's `probes` branch no longer `continue`s past the remaining
branches. A step carrying both probes and per_turn would have had its
turns counted as expected output and never rendered -- failing the run
for a reason unrelated to the run. No step does today; none has to.

Containment now requires the previous turn's REPLY as well as its
prompt. C21 is about the complete rendered history, and a frontend that
appended user messages while dropping assistant replies would have
satisfied the prompt-only check. The verifier covers that case.

The harness_sha256 mismatch is explained inside the repository now, in
phi4_aie4_acceptance_provenance.md next to the record, and pointed at
from the harness header and from a harness_note emitted into every
future record. It was disclosed only in a report under an untracked
directory, so a reader with the repo alone saw an unexplained mismatch
and no way to resolve it.

phi.md pointed users at the token count as the evidence for the missing
history -- the proxy retired last round. It now says what /history
actually shows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dot-source of acceptance_guards.ps1 sat two hundred lines into the
script. It happened to work, because nothing above it called a guard
function -- an ordering nobody had declared, that no test covered, and
that the next edit above that point would have broken with a
CommandNotFoundException in the middle of a hardware run.

It now loads immediately after $script:suiteDir exists, before any other
statement. Verified by running the harness far enough to prove it: it
reaches the Get-FileHash on line 132, which means the dot-source on line
109 succeeded.

That is as far as the harness can be exercised on the development box --
Get-FileHash is not available here, though it is on the target, where
the harness has used it in every run. Worth knowing before someone else
smoke-tests this locally and reads the failure as a defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`$containment = @(Test-HistoryContainment $perTurn)` preserved the function's
comma-wrap, so $missing.Count was 0 on data that has three missing turns. The
only branch that reports a non-accumulating history was unreachable. Step 8
would still have failed for other reasons -- so the run would not have looked
broken, it would have looked like this branch had no headline finding.

Third instance of the same trap, and it landed at a CALL SITE while the round
that shipped it was removing the trap from function BODIES. The verifier could
not catch it because the verifier assigns and the harness wrapped: it tested
guard bodies, and both of the last two shipped bugs were in the statements that
call them.

So verify_acceptance_guards.ps1 now reads the statements. It parses the suite
with the AST parser, derives which functions return a comma-wrapped array from
their source rather than a hand-kept list, and fails on the three forms that
collapse the result -- @(call), bare `foreach ($x in call)`, and piping a bare
call. It has a positive control and two non-vacuity checks, because a lint that
finds nothing is indistinguishable from a lint that passes. Reintroducing the
exact shipped statement turns it red at :1991; 24 call sites across three
files, zero offending after this.

Driving the harness under pwsh -- which round 3 wrongly reported as impossible,
Get-FileHash being absent only under powershell.exe here -- found two more
crashes that had shipped:

- Get-Field threw on a property-less object, which is what a step recorded
  with no evidence becomes after the reload at :3113. Any partial run ended in
  DOCUMENT NOT RENDERED and exit 1, inside the one helper written to degrade
  instead of throwing. The committed -Steps all run escaped it because every
  step supplied evidence.
- Add-UnselectedStep threw on a fresh record, so any -Steps list whose first
  step is not selected died in seconds having recorded nothing. Pre-existing.
  Every step-list read now goes through Get-RecordSteps.

Also: the renderer emitted `### Step 7` twice for a step carrying both probes
and prompt_verbatim, which step 7 does -- measured, 2 headings before, 1 after.
The containment reply probe treats an absent <|assistant|> marker as NOT
CHECKABLE rather than as a dropped reply, and reports that as its own problem
so a template change cannot silently reduce the gate to prompt-only. No verdict
moves on the committed record: still turns 2, 3 and 4.

C20's coverage gap is restated on a project-owner ruling: the ~4k operator
limit is by design, asserting against 4095 is correct, and the criterion's text
is the defect. It stays PARTIAL -- no run can make "bounded at 4096" true.

Step 8 now records the attribution of both multi-turn findings in the record
rather than only in a report: the history loss is upstream FastFlow
(modeling_phi4.cpp:569-573, above the AIE4 #if at :579, shared by seven other
frontends) and documented rather than fixed here; the degeneration of turns 1
and 3 is UNATTRIBUTED, since turn 1 had no history to lose and five single-turn
probes were clean.

Verified offline on both engines, against the committed record. No hardware run
was authorized and none happened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The provenance doc gains the four round-4 harness fixes, three of which would
have damaged the next run, and the measured -- not predicted -- Section 17
movement: replaying the recorded step results through the current harness gives
10 met / 14 partial / 2 not met / 26 not exercised against the record's
12/12/2/26, with C01 and C31 the two that move. It also says to run its
verification commands under pwsh, and why Get-FileHash may be missing from
Windows PowerShell 5.1 on a box where PSModulePath shadows it. The file count
under the suite is named rather than left to prose: three, and why the console
driver is not one of them.

phi.md separates two things it had been running together. The missing history
is an upstream FastFlow CLI-path defect with the source citation -- the
single-message construction sits above the AIE4 #if, so it happens before any
of this branch's code runs, and it is shared by every text frontend. Why two
turns degenerated is not known: turn 1 had no history to lose, reaching the cap
is a consequence rather than a cause, and five single-turn prompts in the same
run came back clean. The document says that instead of implying a cause.

The 4095 limit is presented as designed and cleanly enforced rather than as an
off-by-one to hunt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ning

Two installer defects that had been asserted rather than demonstrated since
Task 11, both found by running `wix build` for the first time in this effort.

`aie4.wxi` declared no XML namespace, so every element inside the generated
<Include> landed in the empty namespace and WiX rejected the file with
WIX0200. That failed BOTH MSI builds -- including the ordinary non-AIE4 one,
which has nothing to do with this feature.

With that fixed, `<Files Include="package\aie4\**" />` doubled the path. WiX
resolves a Files path against the directory of the file containing it, and
aie4.wxi is generated into package\, so it harvested package\package\aie4,
found nothing, and emitted WIX8601 -- a WARNING. The build succeeded and
produced an MSI byte-for-byte the same size as the no-closure MSI: an optional
feature that ships as silently empty. Now 547 MB against 28 MB, with
ryzenai_corelib.dll and its closure present in the File table.

Also ships the provenance with the artifact, per the 2026-09-01 decision.
aie4-closure.txt now opens with corelib_version and corelib_sha256, read out
of the STAGED DLL by calling ryzenai_corelib_get_version through it rather
than trusting the headers the product compiled against -- the field question
about a wrong number is "which runtime produced it", and nothing in an
installed tree could answer it. The version is a hard-coded 0.1.0 across the
whole 0.x series and the probe says so; the hash is the field that identifies.
A probe that cannot load the staged DLL fails the install rather than writing
a placeholder, because the closure it would be certifying does not work.

Both get_files.bat now copy that report deliberately and check it arrived,
instead of carrying it as a side effect of a wholesale xcopy that breaks the
day somebody narrows it. The diagnostic is reached by goto: cmd.exe discards
the exit code of an `exit /b` that runs inside a parenthesised block
containing redirections, so the earlier form printed the error, stopped the
batch, and still returned 0.

test_packaged_runtime drives both shipped .bat files against a synthetic tree
and requires: the report present and unmodified, RED when it is absent, exit 0
when no closure is staged at all, and both aie4.wxi shapes including the two
defects above. That replaces the guard which asserted "the main installer
still builds without AIE4" by forbidding the substring `exit /b 1` anywhere in
either script -- it never checked that property, it checked that the scripts
contain no error handling, and it fired on the correct code above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cesses

Design 12.4's concurrency properties -- a unique file per process, a pending
record preserved while its owner is alive, ordered reporting of every
completed record -- were checked only inside one process, against PIDs, start
times and a process probe the test supplied itself. That left the production
`ProbeProcessStart` unexercised: nothing had confirmed it recognises a
genuinely live sibling, and that single decision is all that stands between a
running process's pending record and its deletion.

Two concurrent children now take real PIDs and real start times from Windows,
hold their pending files open while the parent drains, and are required to
survive that drain; then both persist and both must be reported, in filename
order derived from the paths rather than assumed from start order. The
children deliberately do not load corelib -- two AIE4 device contexts at once
fail in ways that look like defects, and none of these properties needs a
device.

Not claimed: "safe preservation when the process-start query fails". A failing
Win32 query cannot be produced on demand, so that case stays with the
injected-probe test.

Verified by mutation: removing the children's wait makes the scenario fail
with a diagnostic naming the absent pending file. That diagnostic replaces a
bare CHECK, because the bare one sent me down a wrong theory for an hour --
it could not distinguish "the child already persisted" from "the store lost
the file".

Two robustness fixes found on the way, both real, neither the cause of the
failure I was chasing:

  - the parent waited on `pending.marker` existing, but ofstream CREATES a
    file before it writes to it, so a zero-length path could be read. A
    `ready.marker` written last, after the others are flushed and closed,
    means "the markers are complete" rather than "a marker has begun".
  - the temp directory was named from the parent PID alone. Windows reuses
    PIDs, and a failed run could leave children polling that directory, so a
    later run could share it with somebody else's children. It now carries a
    tick count, and a scope guard terminates both children on every exit path.

The intermittent failure itself was neither of those. It was my own mutation
harness: `Copy-Item` preserves the source's timestamp, so restoring the
pre-mutation file left it OLDER than the object built from the mutant and
MSBuild skipped the rebuild. Every "restored" run was still running the
mutant. With the source touched and the rebuild confirmed by the executable's
timestamp, the scenario passes 10 of 10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…failures

Step 10 documentation, plus a correction to how the acceptance record
characterises its own worst result.

phi.md gains what a user has to know before choosing this tag: that it is
Windows and AIE4 only; that the existing NPU2 tag `phi4-mini-it:4b` is
untouched; what the optional runtime installs and that aie4-closure.txt names
the corelib version and hash that produced any result; that there is NO
fallback, with the two distinct refusals a user can actually see; the 4095
usable cap against a 4096 KV window; model load and its 86% helper-interrogation
share; cold and warm TTFT; prefill and decode throughput; peak host and
accounted device memory; the 128-token post-warm stability window; V-scatter
calls, bytes and time; the append/re-prefill threshold with its measured cost
both ways; and terminal restart behaviour with the fatal record's location and
contents.

Two measured runs are shown side by side rather than one being chosen. This
machine has been measured moving 1.8x on decode with no code change, and a
single column would read as more precise than the measurement is.

The Task 15 re-measurement is published beside the rendered baseline, not into
it, and phi4_results.md says why: report_phi4_corelib_baseline.py refuses to
render because the committed determinism records now span three harness
binaries and DETERM-3 forbids pooling across binaries. That is the rule working
-- and it means the determinism baseline cannot be re-rendered from the
accumulated set at all, since every rebuild adds another. Two further
consequences are recorded: the published claim that the crossover bracket's
LOWER edge is stable does NOT hold at history 2048 (a third interleaved run
puts it at 24, not 12), and on this run alone Section 10.7's rule would select
8 rather than the shipped 4. The constant is left at 4 -- it is still inside
the append-wins region on this run, and raising it is a human recalibration
decision, not a regression-pass edit.

The record's characterisation of Step 8 is corrected in the provenance file
rather than by editing the record. It gave BOTH failed turns the single reason
"the reply is dominated by a repeated token". Re-derived from the record's own
reply_verbatim: repetition is common to both and is ordinary degeneration; what
is not ordinary happened once, in turn 3, which produced language for 85% of
its reply and then broke mid-word into high-entropy punctuation at character
10,200 of 23,802. The prompts were 13 and 9 tokens, so the 4,095 totals are
output alone.

Test-LooksLikeEnglish could not see it: turn 3 is 71.6% letters overall because
the clean part carries the average. It now also scans in 200-character windows
and reports the first window that stops being language, with its offset -- the
only precise entry point a later investigation has. It moved into
acceptance_guards.ps1 so the verifier drives the SHIPPED function over the
SHIPPED text instead of restating the rule; on the committed record it fires on
turn 3 at 10,200 and is silent on turns 1, 2 and 4, with a long alphabetic
control to stop it firing on length alone. Verifier now 37 assertions, green on
Windows PowerShell 5.1 and pwsh 7.

Recorded as a known open issue, accepted, not investigated here. Normal-length
output is sound: five single-turn probes, all coherent and correct in about ten
seconds. The attention-kernel hypothesis carried in both documents is
attributed to operator experience and explicitly NOT a finding of ours.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tch file

The lines added for the closure-report copy and its diagnostic were written
with bare LF into a file that was CRLF throughout, leaving 30 CRLF and 20 LF
line endings in a shipped .bat. cmd.exe ran it -- the packaging test drives the
real script and passed -- but mixed endings in a batch file are a trap,
particularly around the `goto` the diagnostic now uses, and the file had no
such mixture before this branch touched it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The probe runs from a POST_BUILD command, so it inherits MSBuild's MSVC
environment. Add-Type compiles C#, the C# compiler reads LIB, one of the
Windows Kit entries is relative, and PowerShell runs the compiler with
warnings as errors:

  Warning as Error: Invalid search path 'lib\um\x64' specified in
  'LIB environment variable'

So the staging step failed, and with it the whole flm build. Every test I ran
against this probe passed because they all ran it from a plain shell, where
LIB is unset -- it only failed once the committed tree was rebuilt end to end.
LIB and INCLUDE are cleared around the Add-Type and restored afterwards;
neither is needed to compile a P/Invoke declaration. Verified by running the
probe under vcvars64 (returns 0.1.0, exit 0) and by a clean feature-ON build
whose staging step now succeeds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hat catches it

Before this branch, src/CMakeLists.txt defined CURL_STATICLIB / BOOST_ALL_NO_LIB /
BOOST_ALL_STATIC_LINK unconditionally on the WIN32 AND NOT VCPKG_TOOLCHAIN path and
hard-coded the static b2 spelling libboost_program_options-vc143-mt-x64-1_88. Commit
0db2c3a put the macros behind option(FLM_WIN_STATIC_DEPS ... OFF) and replaced the
name with a search preferring the SHARED spelling first.

FLM_WIN_STATIC_DEPS is set ON nowhere in the tree, no preset sets a toolchainFile so
VCPKG_TOOLCHAIN is false on every Windows preset, and windows-build.yml runs
`cmake --preset windows-vs18` bare. The shipping Windows binary's Boost linkage
therefore changed from static to shared, silently, and nothing in the repository
stages boost_program_options.dll. That is why the MSI is missing it -- not, as the
Task 15 report recorded, FastFlow's own packaging list.

- default FLM_WIN_STATIC_DEPS to ON, the configuration CI and the MSI actually ship;
- derive the boost_program_options name order from the same variable that picks the
  macros, so the two halves of one decision cannot disagree (curl deliberately gets
  no such treatment: libcurl.lib is the static archive in a vcpkg static triplet AND
  conda-forge's import library, so its names carry no linkage information);
- add a configure-time FATAL_ERROR that fires in both directions when the resolved
  spelling contradicts the macros, naming both halves and both ways out.

Also I12: the feature-OFF compile guards no longer need corelib present to configure,
and the feature-ON and unconditional Boost hint lists -- whose comment claimed they
were the same search and which differed by %USERPROFILE%\anaconda3\Library\include --
are now one list used twice.

tools/tests/test_windows_dependency_linkage.py goes red under three separate
mutations: default flipped to OFF, shared-first order against static macros, and the
consistency guard deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I4 -- test_packaged_runtime.ps1 printed "7 block(s), none skipped" while the AIE4
model load had never run. The `if ($RunAie4ModelLoad)` had no else and no $skipped
entry; the only mention of the flag was on the branch where -FlmExe is ABSENT, i.e.
exactly when it is irrelevant. -RunAie4ModelLoad is passed by nothing in the tree, so
the block has never executed. Add the else, and stop the skip message inviting the
operator to pass a flag that would have hung the suite: the block piped "/bye" into a
REPL that reads via ReadConsoleInput and cannot see a pipe. It now goes through
drive_flm_console.ps1, which Task 16 proved against the real binary.

Task 15 review I2 -- the WiX namespace assertion covered only the closure-present
aie4.wxi. get_files.bat emits <Include xmlns=...> in both branches, and the one that
was unguarded produces the ordinary non-AIE4 MSI: the branch WIX0200 broke. Deleting
the xmlns from the else branch sailed past the old guard and now fails.

Task 15 review I1 -- ReadCorelibVersion.ps1's LIB/INCLUDE save/restore fixed a real
product-build breakage and was verified once, by hand, in one environment. Nothing
referenced the script and no test set LIB. The new corelib-version-probe block runs
the real script under powershell.exe BY ABSOLUTE PATH with an MSBuild-shaped LIB
containing a relative path; invoking 5.1 explicitly is load-bearing and measured, not
assumed, because pwsh 7's Add-Type uses Roslyn and ignores LIB, which would have made
the check vacuous. The comment states what is and is not covered: the success path
needs a real ryzenai_corelib.dll and is not covered here.

Both new guards were proven red against scratch copies of get_files.bat and
ReadCorelibVersion.ps1; both files are byte-identical before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C1 -- phi4_aie4_acceptance.md was the only .md under docs/ beginning EF BB BF.
Jekyll matches front matter against the raw first line and does not strip a BOM, so
the branch's headline evidence had no layout, no nav entry, no pretty URL and no
entry in search.json. Cause: Set-Content -Encoding utf8, which under Windows
PowerShell 5.1 means UTF-8 WITH BOM. The harness's read-back stamp check could not
see it, because every text reader in PowerShell strips one. The document is now
written through Write-Utf8NoBom and the harness asserts the byte itself.

C2 -- $rank['<unknown>'] is $null and $null -gt 0 is $false, so an unrecognised step
status rolled up to `met` and reached none of the counters feeding the exit ladder:
RESULT: ACCEPTED, exit 0. Reproduced in 5.1 and pwsh 7 for 'failed', '' and 'bogus'.
run_hardware_suite.ps1:941 fixed this shape and called it "the tenth instance"; the
fix had not reached the harness that writes the record. The rank table and the check
now live in acceptance_guards.ps1 so the verifier drives the same code, every
recorded step is checked rather than only those a criterion cites, and an unknown
status is a hard failure written to stdout before it throws -- a top-level throw
under `pwsh -File` exits 1 silently.

I3 -- the corelib source revision was operator-typed free text asserting a pristine
tree that every sibling artifact recorded as -untracked-only. Get-GitRevision moves
into acceptance_guards.ps1 and is now the only definition: run_hardware_suite.ps1
dot-sources it and its private copy is gone. The harness derives the revision,
records how it was obtained, aborts if an operator-supplied value contradicts the
checkout, and labels an underivable one -operator-supplied-unverified. The committed
record's wrong value is NOT edited; the correction is on the provenance page.

I5 -- three identity cells assigned only inside `if ($npu)` were read raw under
StrictMode, so a healthy ninety-minute run ended in DOCUMENT NOT RENDERED on any box
whose NPU friendly name does not match. Routed through Get-Field.

I2 (renderer half) -- the record called the divergence "bounded" and explained that
non-associative addition "varies the last bits". Both point away from what was
measured. The renderer now writes the correction itself, so an older record is
corrected rather than rewritten, and says both halves of "bounded" fail: the
divergent runs breached the DETERM-2 per-value bound, and greedy decoding would not
have contained the effect inside it.

M-FIX-2 -- the record now links the provenance page that corrects it.

Also in run_hardware_suite.ps1, from the same review:

I10 -- the suite skipped `flm run` for a reason Task 16 disproved, and because the
skip fired on every run whichever way -FlmExe went, `$skipped.Count -gt 0` was always
true and `exit 0` / PASS was unreachable: a full hardware run and a run that only
built the tests both returned 77. The skip is gone; Step 7 drives the REPL through
drive_flm_console.ps1 and measures the reply after stripping the echoed keystrokes
and the next prompt -- the first version of that measurement passed on an empty
reply. Reachability of exit 0 is re-established by AST enumeration of every remaining
`$skipped +=` site plus executing the verbatim ladder.

I11 -- `$d.localisation` was read raw outside any try under StrictMode, so a record
without that key killed the script after hours of device time and the summary and
exit ladder never ran; it now goes through the file's own Get-JsonField. And the
baseline report was gated on Test-Path against a FIXED path, so a stale file from an
earlier run made the committed phi4_results.md render from data this run did not
produce while claiming it did; the stale file is now deleted up front, an "exited 0
but wrote no --output-json" assertion added, and rendering gated on the run having
produced it.

Found while fixing the above, each an instance of the same defect class:
- the BOM detector's positive control was vacuous under pwsh 7, because it built its
  "file with a BOM" using the very cmdlet whose behaviour differs between hosts;
- the document did not render the same on two hosts: ConvertFrom-Json parses the
  ISO-8601 `utc` field into a [datetime] under pwsh 7, so "Run (UTC)" came out as the
  machine's LOCALE short-date, losing the sub-second precision and the Z;
- the provenance link added by this commit was itself broken, because under
  `permalink: pretty` a page is served from a directory and a relative link resolves
  one level too deep.

rerender_acceptance_document.ps1 executes the harness's own render block against a
committed record -- located by anchors, with its two helpers lifted by AST, so
nothing here is a copy of the renderer. The verifier drives it and requires the
committed document to differ from a fresh render in exactly one line, the render-time
stamp. That is the check that would have caught the BOM.

verify_acceptance_guards.ps1: 37 assertions (35 coverage) -> 72 (67 coverage), green
under both Windows PowerShell 5.1 and pwsh 7. Every new guard was proven red by
mutation and the tree restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eted synchronize

I13 -- the same parsed field was read with two meanings. GenerationLoopLimit returns
an explicit -1 verbatim and generate_aie4's `length_limit > 0` correctly ignores it,
but RequestedMaxNewTokens returned it as a requested token COUNT and
validate_aie4_capacity refused any negative value. So Ollama's documented
"generate forever" sentinel returned 400 on AIE4 and 200 on legacy, and omitting the
field gave exactly the unbounded behaviour that asking for it refused.

RequestedMaxNewTokens now returns std::nullopt for any non-positive explicit value,
matching CliRequestedMaxNewTokens, which already had the right idiom.
GenerationLoopLimit is unchanged. 0 is in the same bucket deliberately, because both
loops gate on `length_limit > 0` and reserving 0 at admission while the loop runs to
the cap would be the two disagreeing about one request; the LOOP behaviour for 0
(branch-review Minor 22) is not changed -- that is a question about both backends --
but it is now pinned by test rather than holding by accident.

Tested where the requirement asked, on both paths: the new frontend test builds a
real AIE4-backed and a real legacy-backed Phi4 in one process from the same package,
drives both from actual /api/chat bodies, and pins the observable outcome (admitted
vs ModelRequestError(400)) and both loop limits for absent, -1, -2, 0, 64 and an
over-cap value. The over-cap case is there so a "fix" that deleted the admission rule
fails rather than passes.

I14 -- StepSubmissionState was latched for the whole step, so after the first
successful dispatch every host-side operation between the remaining ~100 -- the V
scatter, every padding bridge, PrepareLastHidden, and ReadLogits, which runs AFTER
the final synchronize -- terminated the process on a bad_alloc where clearing the
session and returning 500 would have sufficed. checked_synchronize() now clears the
state after the status check, so the classification matches the physical state.

This goes further than the review recommended and the owner should know it: rejected
DISPATCHES following a completed synchronize, including the flat_mha 4096-window
refusal, also move to the recoverable side. There is no principled way to hold both
positions -- checked_submit already treats a rejected FIRST dispatch of a step as
recoverable. The physical premise was verified against the corelib sources rather
than assumed: Stream::Synchronize waits every outstanding command and empties its own
list, every refusal precedes its call's single enqueue point, and Stream::Submit pops
the command back on a throw. A FAILED synchronize stays on the terminate side.

Pinned both ways: four recoverable cases, one of them 13 synchronizes into a step,
each asserting no terminator, runtime Healthy, position not advanced and a clean
prefill afterwards; plus a new terminate case where layer 1's q has submitted after
four completed synchronizes and layer 1's k is rejected. Comments made false by the
change were updated rather than left standing.

ctest: 16 passed, 0 failed, 4 correctly skipped for absent hardware.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ta falsifies

I1 -- the three links this branch added were the only .html internal links under
docs/, and docs/_config.yml sets `permalink: pretty`, so all three 404'd. Replaced
with the repo's site-absolute trailing-slash form. Neither new page was in
site.docs_nav or benchmarks/index.md -- their `parent:` front matter is inert, this
is not just-the-docs -- so both are added to each.

I2 -- no user-facing page said that two runs of the same binary emitted different
text. phi.md's Known limitations now leads with it: 3 of 150 committed run-pair
records, per-record maxima 48.34 / 49.25 / 49.21, per-record worst differing-logit
counts 196,154 / 196,748 / 196,835, all three on the append route, all three paths
given, and the instruction not to use output equality as a test oracle. Every figure
was re-derived from the committed JSON; the branch review's second record path and
its "token 8" generalisation were both wrong and are not reproduced here.

I8 -- phi.md said 4096 in one place and 4095 in another while the code admits 4,094.
It now leads with "Size prompts against 4,094 tokens, not 4,096", agrees with itself,
cites Phi4::validate_aie4_capacity, and explains that the catalog's 4096 is the KV
window. The 4096-row prefill measurement is kept and footnoted as engine-level, not
submittable. The mitigation for the headline known issue named no mechanism that
exists: there is no CLI flag, so it now names `/set gen-lim <value>` in the REPL and
max_tokens / options.num_predict over HTTP.

I9 and Task 15 review I4 -- the falsified lower-edge stability claim was live in five
places. Rather than hand-patching a forward pointer, the generator was fixed: the
hardcoded stability strings in build_document_section are gone, the block re-renders,
and the live text now reads "It is NOT a measured constant", "nothing here may then
call that edge stable" and "3 of 3". Two claims added to _RETRACTED, and the
retraction scan widened from one block to the whole committed document. Both went red
against the unfixed document first.

Two further defects in that guard, found while fixing it: the document matcher was
not whitespace-collapsed, so a wrapped claim scored zero (the Task 14 parked ruling)
-- now fixed with a wrapped-claim test that failed before; and the blockquote
exemption was laundering the generator's own voice, because
crossover_stability_narrative returns its guidance AS a blockquote.

Task 15 review I3 -- the 2026-09-03 run had never been appended to
phi4_aie4_crossover_history.json, so the calibrator kept evaluating cross-run
agreement against only the two runs that agree. The record is DERIVED from the
committed rerun JSON by the project's own crossover_entry() and
merge_crossover_history(), with a byte assertion that the four existing records
round-trip first; the diff is insertions only. The durable guard is a test that
re-derives it and requires it present.

Task 15 review I5 and I8 -- the V-scatter call counts (231,264 / 1,850,112 over 7,227
model steps, counts_are_measured) are published; and the "8" is published as fragile,
with both refusal grounds named and the p95 margin shown next to its p50 counterpart,
a 0.4 ms difference the other way against 19.7 ms of uncertainty.

I6 -- CI is out of scope to build and is recorded as a known gap on the provenance
page, with all five facts verified, plus one more: debian-portable.yml:122 defines a
job named `test-summary` that runs no test and echoes four hardcoded tick marks into
the step summary. Upstream, documented, not fixed.

The provenance page also carries the correction to the acceptance record's
operator-typed corelib_source_revision, since that record is not edited after the
fact, and a note that an AIE4 build against a shared prefix now needs
-DFLM_WIN_STATIC_DEPS=OFF.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
M-FIX-1 -- src/model_list.json was reformatted wholesale for a 3-value change: a
json.dump(indent=2, sort_keys=True) round-trip across 2,234 lines of the product's
most actively edited catalog, semantics-preserving but guaranteed to conflict with
any concurrent model addition and impossible to review. Rebuilt from 2d6c483 with
the single entry re-applied in the base file's own style: `git diff --stat 2d6c483`
goes from 2234 ++++---- to 59 +++++++, insertions only, one difflib opcode, and the
object graph is identical to HEAD's.

The tool regenerated it, so the churn would have come back. package_phi4_corelib_aie4
no longer dumps the whole file; it splices the one member, with a runtime round-trip
proof -- the produced text must parse to the requested graph AND reduce byte-for-byte
to the input when the entry is stripped again -- and nothing reaches disk if either
check fails. Style-matching was ruled out by measurement rather than by preference:
the committed catalog is not the output of any json.dumps call, and a re-dump at the
original indent and key order still rewrites 66 lines. model_info.json is written by
the same function and had the identical latent defect, so the writer covers both;
src/model_info.json itself is untouched. Also fixed a re-run clobber that wrote the
stale entry back after inserting the fresh one.

One correction to the review: the LF->CRLF half of the charge does not survive
checking -- all three blobs are LF in the index and core.autocrlf converts on
checkout. The indentation and key-order halves are real.

M-TAKE-1 -- .build-task* is gitignored. The existing /.build-*/ is root-anchored and
did not cover src/test/phi4_corelib_aie4/.build-taskN/, which is where the suite
actually drops them. Verified nothing tracked is caught: git ls-files unchanged at
1116 and `git ls-files -i -c` empty.

M-TAKE-2 -- MIN_CORRELATION and MIN_DECODE_STEPS are sealed, extending _DETERM2_SEALED
in kind rather than adding a second mechanism. Values became (literal, reason) pairs
because the existing message -- "the largest differences ever measured on this
hardware" -- is true of the two DETERM-2 bounds and false of the two 12.4 thresholds,
and one message fitting both would have been true of neither.

There was no test for _DETERM2_SEALED at all: no .py file references the comparator
and its only caller needs an AIE4 device, so the seal's failure path had never been
executed by anything automated. The new test tampers with each constant the way
someone would after grepping a failure message, and includes
test_every_threshold_quoted_in_a_failure_message_is_sealed -- the assertion that would
have caught this omission in the first place.

Python tooling suite: 280 -> 309 tests, OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I14 -- the code change is KEPT; the retraction of the rule it replaced had
reached one comment and stopped. The tree still asserted the old per-step
irrevocable boundary in seven user-visible error strings, the classifier's own
name, two headers, a published page and the design spec.

  * The six HTTP-500 strings in modeling_phi4.cpp and the CLI notice in
    generation_limit.cpp said "AIE4 inference failed BEFORE SUBMISSION". That
    was true by construction while the boundary was per step; it is false for
    exactly the cases this round added -- a bad_alloc in the V scatter of layer
    0 arrives after three submits and one synchronize. They now say "at a
    recoverable point", which is what the engine actually decided and all the
    caller can act on. test_generation_limit.cpp pinned the old wording and
    went RED on the source change before it was updated; it also now asserts
    the string does NOT contain "before submission".
  * RecoverableBeforeSubmit -> RecoverableBeforeIrrevocableWork, with a note
    that submission.irrevocable() means "work may be outstanding", not "a
    submit has happened".
  * modeling_phi4.hpp:39-40 said reaching the cap "takes the process with it";
    its .cpp twin was rewritten this round to say the opposite.
    phi4_corelib_constants.hpp said the flat_mha refusal "TERMINATES THE
    PROCESS", flatly contradicted by phi4_corelib_aie4.cpp:735-750. Both now
    state the 2026-09-02 measurement as history and the current rule as
    current, and both keep the reason the bound is still not optional: reaching
    flat_mha costs the caller the whole conversation either way.
  * phi.md's user-facing paragraph now names a completed synchronize as the
    other recoverable case.
  * Design 6.6 FAIL-2 is per submission group, with the Stream.cpp facts it
    rests on; FAIL-3 says why a FAILED synchronize stays terminal; a new FAIL-4
    covers the reclassified case; 13.2 is amended to match. (The spec lives
    outside this repo, under docs/superpowers/, which is gitignored here.)

NEW-1 -- test_windows_dependency_linkage.py was a spelling check. A
`"FLM_WIN_STATIC_DEPS": "OFF"` in CMakePresets.json re-breaks CRIT-3 exactly and
kept all nine tests green, because the file was never opened. It is opened now,
every configure preset is scanned (cacheVariables are inherited, so any preset
counts), and there is a non-vacuity assertion that windows-vs18 was actually
found. A `set(FLM_WIN_STATIC_DEPS OFF ... FORCE)` after the option reaches the
same place without editing the option line, so that is asserted against too.
Both re-breaks were applied and each produces exactly one failure.

NEW-2 -- the second find_library now carries C:/dev/boost_1_88_0/stage/lib and
C:/dev/vcpkg/installed/x64-windows/lib as PATHS. target_link_directories
contributes nothing to find_library -- probed again here: a stage directory
known only that way yields NOTFOUND, the same directory on PATHS resolves -- so
replacing the pre-branch raw names with find_library left the shipping configure
resting on the runner's ambient LIB where it previously rested on nothing.
PATHS is the last bucket searched, after HINTS and CMAKE_PREFIX_PATH, so this
cannot displace a prefix supplied deliberately.

  OPEN QUESTION, NOT SETTLED HERE: whether the self-hosted Windows runner has
  an ambient LIB that already covers C:/dev/boost_1_88_0/stage/lib is unknown
  from this machine, and `echo $env:LIB` on the runner is the one command that
  answers it. This change is correct either way -- it removes the dependency
  rather than proving it was unmet -- but nothing below should be read as
  evidence about that runner.

NEW-3 -- TOP_K = 32 was live and unsealed while the "every threshold is sealed"
test compared the seal against a literal typed into the test file, so the
universe and the expectation were the same hand-maintained list. TOP_K is now in
_DETERM2_SEALED (it is the WIDTH of the window MAX_TOP32_ABS_DIFF is measured
over: shrinking it trips no bound, it just stops the comparison looking), and
the test derives the universe from the comparator source instead. Dropping
TOP_K to 2 now aborts the tool at import. The seal suite also normalises CRLF,
without which a fresh Windows clone turns every tamper case red for reasons
unrelated to the seal -- observed on this tree.

NEW-4 -- the re-render guard reported PASS on a failed render. The harness's
render block writes the markdown BEFORE its BOM check, stamp read-back and
Test-RenderedDocument, so a failed render still leaves a complete document and
`Test-Path` is true for it; the child's exit code was discarded. It is checked
now, with the file's presence kept as a separate condition. Demonstrated: with a
throw injected after the document is written, the check reports
"exit 1, document written = True" and the verifier exits 1 where it used to pass.
The lift guard's error text, which claimed a ConvertTo-PlainData rename "would
silently render nothing", is corrected -- the render block never calls it.

NEW-5 -- the REPL hang Task 16 removed from test_packaged_runtime.ps1 was still
live in the acceptance harness. Step 1c justified reading the screen instead of
the exit code with "under a redirected stdin the REPL prints its banner and
returns 0 on the first ReadConsoleInput". cli_wide.cpp:107-113 `continue`s on a
FAILED read inside while(true): it spins, it does not return. On a rejected load
flm exits by itself, which is the only branch the committed record ever
exercised; on a SUCCESSFUL load -- the not_met case the step exists to detect --
it would have hung the ~90-minute harness. Step 1c now drives the console driver
like Steps 6/7/8, which is bounded in both directions because Wait-ForScreen
returns as soon as the child exits. Invoke-FlmConsoleSession moved above Step 1c
accordingly: PowerShell resolves a function at call time from what has already
been parsed.

verify_acceptance_guards.ps1 had no automated caller anywhere -- not a
CMakeLists, not a .cmake, not a workflow. Seventy-two assertions that ran only
when a human remembered. It is now `test_acceptance_guards` in the suite's own
CMakeLists, registered BEFORE the corelib-less early return because it needs no
device, no corelib, no model and no flm.exe. No SKIP_RETURN_CODE: it has no
input it can be missing.

Verified: ctest 17 passed, 0 failed (was 16), the same four Skipped;
`python -m unittest discover -s tools/tests` 312 OK (was 309);
verify_acceptance_guards.ps1 exit 0 on both Windows PowerShell 5.1 and pwsh 7,
72 assertions. Every guard above was made to fail before it was left passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
History is not rewritten, so this is the correction rather than an amend.

Commit 314a635's message says, of the determinism figures:

    "the branch review's second record path and its 'token 8' generalisation
     were both wrong and are not reproduced here"

The first half is FALSE. final-branch-review.md:169 reads

    max abs logit diff 48.34 ; 196,154/200,064 logits differing on one step

directly beneath
determinism_records/20260902T120919Z-10264/determ1-force_append-010.json, and
names -022 (49.25) and -026 (49.21) on the next line. All three paths and all
three path/figure pairings in that review are correct. It did not mispair
196,154. `git log -S"196,154" --all` shows the figure has only ever appeared
attached to -010, so no committed state ever carried it elsewhere either.

Re-derived here a fourth time, independently, from the committed JSON:

  determ1-force_append-010  17 breach lines  max 48.34375     196,154 @ decode[13]
  determ1-force_append-022   8 breach lines  max 49.25        196,748 @ decode[15]
  determ1-force_append-026  10 breach lines  max 49.21484375  196,835 @ decode[15]

all against determ2_bound_ulps = 2. These agree with the re-review's two
independent derivations and with what is published.

The mispairing originated in the documentation workstream's own draft and was
attributed outward from there. The PUBLISHED pages are right; it is the account
of where the error came from that was wrong, and that account also reached
final-fix-report.md, which is corrected in place (untracked, under
.superpowers/).

The OTHER half of the sentence stands: the review's "diverges at token 8" was a
generalisation from one record. -010 parts at the 8th emitted token; -022 and
-026 both part at the 14th. phi.md says so, and that correction was earned.

No file in this commit changes behaviour or evidence. It exists so the record
does not keep a false charge in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…path

Condition 4 -- the comment beside checked_synchronize asserted that a rejected
dispatch is recoverable for flat_mha, o, ssmlp AND lm_head. Only flat_mha had a
test. A comment claiming four cases on evidence covering one is a test agreeing
with the implementation about which case exists, which is the failure this
branch keeps producing. The other three are tested now.

Each sits immediately after a checked_synchronize in RunRows, so each is a
rejected dispatch with corelib's outstanding list provably empty:

  o        layer 0, after the qkv and flat_mha synchronizes    2 synchronizes
  ssmlp    layer 0, after o's synchronize                      3 synchronizes
  lm_head  the last dispatch of the step                     128 synchronizes

The synchronize count is pinned as well as the call and the status, because
"after a completed synchronize" IS the claim -- a case that quietly moved to a
different point in the step would still throw and still look green. lm_head at
128 also re-proves the flag re-arms across all 32 layers; if it failed to
re-arm anywhere in that span, the case would never reach lm_head.

Earned, individually: with `submission = corelib::StepSubmissionState{}` deleted
from checked_synchronize, each of the three was run in isolation and each
TERMINATES --

  call=o        phase=o        layer=0     detail=injected o refusal ...
  call=ssmlp    phase=ssmlp    layer=0     detail=injected ssmlp refusal ...
  call=lm_head  phase=lm_head  layer=none  detail=injected lm_head refusal ...

-- exit 1 in all three. Restored, all seven recoverable cases pass.

Also made true a claim the same test block was already making: its comment said
each case "asserts that no fatal record was written" and nothing did.
EngineFixture::AnyFatalRecord() checks existence without parsing (FatalRecord()
throws when there is none, which is the wrong shape for asserting absence), and
check_recoverable now asserts it.

Condition 3 -- TerminateHostFailure and TerminateUnknownFailure are now
UNREACHABLE from RunRows. Recorded at the call sites rather than removed, with
the reasoning, because the functions are still live from ~Impl's destruction
path and because deleting them is the wrong response.

Reaching either needs a non-CorelibError exception thrown while
submission.irrevocable() or synchronize_in_progress is true. Both windows
contain only Submit* forwarders -- bare calls through the C function table, no
allocation, no container -- plus api->Check, which throws CorelibError and
nothing else. Every host operation that CAN throw a std::exception (StageInput,
ScatterValue, BridgePadding, PrepareLastHidden, ReadLogits, and the
`active_phase = "..."` assignments, which allocate for the longer literals) sits
either before the step's first submit or after a completed synchronize. So the
recoverable guard is always true when a host exception arrives, and both arms
rethrow.

The note says what would make them fire again -- any host work inserted BETWEEN
a checked_submit and its checked_synchronize -- which is precisely the case that
must still terminate. That is why they stay: deleting them would silently
reclassify the next such edit as recoverable. One residue is stated rather than
tested: api->Check allocating its own message and throwing std::bad_alloc inside
the synchronize window would reach TerminateHostFailure.

It also records what went unmentioned when it happened: the two tests asserting
that a host failure past a submit terminates were DELETED this round. That was
correct -- they asserted the retracted rule -- but the `phase == "v_scatter"`
string assertion went with them, and no fatal record can carry that phase any
more, so active_phase's v_scatter value is now only reachable on the dead path.
The same three failures are covered from the recoverable side instead.

NEW-7 remains deferred by owner ruling.

Verified: ctest 17 passed, 0 failed, the same four Skipped;
`python -m unittest discover -s tools/tests` 312 OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ns that had gone stale

Two new top-level pages. The implementation page states what ships -- the
corelib ABI surface actually bound, the model constants, the runtime
resolution rule, what was verified on which machine. The caveats page states
what to watch out for: ten items, each measured or explicitly marked
unverified, with the workaround where one exists.

The caveats page leads with the MSI not being self-contained, because that is
what the next person to install it on a clean box will hit first: 0xC0000135
before any message. Verified by execution, not inferred.

While writing them, five citations in phi.md were found pointing at lines that
had moved during the fix rounds -- validate_aie4_capacity twice, and the three
that carry the upstream-history-loss argument. All corrected. The new pages
say plainly that line numbers in modeling_phi4.cpp have already moved once and
that the ordering is the claim, not the numbers, so a future drift degrades
into a search rather than into a false citation.

Both new files: no BOM, CRLF to match the rest of docs/, no .html links under
permalink: pretty, nav_order 5 and 6 with no collision.
Ruled out of scope: this backend is not built in CI, so which of the two
possibilities held on the build host will not be established. The section no
longer reads as a task with a command to run.

What it records instead is the mechanism, which is the part that stays true:
find_library and the linker search different places, target_link_directories
feeds only the second, and this work moved Boost resolution from the second to
the first. That is probed, not assumed. The explicit PATHS in
src/CMakeLists.txt make it resolve regardless, and the section now says not to
delete them just because a build succeeds without them -- on some hosts it
will, for a reason that is not in the repository.
@chizamd
chizamd marked this pull request as draft September 4, 2026 01:37
chizamd and others added 3 commits September 3, 2026 18:37
The AIE4 acceptance campaign ran on one lab machine with a device and the
real model. Nothing in the tree or in CI could run it, so what merged was
evidence of one campaign rather than a gate on future changes -- roughly
24,000 lines of it, a third of the diff.

Removed:
  - the end-to-end and benchmark harnesses (test_phi4_e2e,
    benchmark_phi4_aie4) and their CMake targets
  - run_hardware_suite.ps1, run_real_model_acceptance.ps1,
    acceptance_guards.ps1, verify_acceptance_guards.ps1,
    rerender_acceptance_document.ps1
  - the add_test(test_acceptance_guards) registration
  - 157 determinism records and phi4_aie4_acceptance.json

Kept, because a registered test reads them and they are fixtures rather
than dead evidence: phi4_aie4_baseline.json,
phi4_aie4_baseline_task15_rerun.json, phi4_aie4_crossover_history.json.
Deleting the rerun baseline broke
test_the_run_that_moved_the_lower_edge_reached_the_history_file, which
guards a defect that actually occurred -- a measurement written up in
prose and never appended to the file the tooling reads -- so it came back.

Kept for the same reason: drive_flm_console.ps1, invoked by the registered
test_packaged_runtime, not campaign-only as a first pass suggested.

corelib_phi4_manifest.json is marked -diff in .gitattributes rather than
re-serialised compactly. Both hide 10,556 lines from review; only one
changes the bytes, and those bytes are pinned by size and SHA-256 in
model_list.json. Nothing about the file changed: 274527 bytes,
sha256 09cee6ef..., as recorded.

Docs keep every rendered conclusion. The acceptance and provenance pages
now say up front that the paths they cite resolve at 5c93aad; caveats
gains item 11, stating plainly that these figures are reports rather than
artifacts a reader can re-derive.

275 files / +73,820 -> 110 files / +49,620.

Verified: cmake configures clean and registers 16 tests; pytest 312
passed. NOT verified: ctest execution, which needs XRT and a corelib
runtime absent from a development box -- it must be re-run on
xcomedusad-43.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ran on 43 at a92c1f6 against the real corelib: 13 passed, 3 skipped,
0 failed of 16 registered. test_real_corelib passed in 11 s against the
actual ryzenai_corelib.dll; the three skips want FLM_AIE4_HARDWARE=1 or a
staged installer.

The first run came back 15 passed / 1 failed. The failure was
test_phi4_continuation_calibration hitting its deliberate no-Python
branch -- on that target PATH resolves python3 to a Cygwin symlink
Windows cannot execute, and run_hardware_suite.ps1 was the thing that
used to pass -DFLM_PYTHON_EXECUTABLE. Removing the campaign removed
that, so the flag is now the caller's job and the build recipe says so.

Nothing in the code changed; this is the verification the previous commit
said was outstanding, plus the gap it exposed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three changes, all to what a reader sees rather than to what runs.

Removed docs/docs/benchmarks/phi4_aie4_acceptance.md and its provenance
companion (974 lines). They were a step-by-step record of one campaign's
internal review, published into the site navigation beside the model
benchmark pages -- a page titled "acceptance provenance" listing where our
own record was wrong does not belong next to "Qwen3 results". Their
substance survives: the tally is in the implementation page, the corrected
turn-3 reason is caveat 5, the "bounded" overstatement is caveat 4, and
the three corrections are named in the caveats closing note. The nav
entries in _config.yml and benchmarks/index.md revert to base.

Cut ~70 lines of process narration from phi4_results.md: the internal task
number in a heading, an account of which claims moved in which review
round, and a section explaining why a generator declined to render. Kept
every measurement -- the second-run comparison table, the three-run
crossover table that falsifies the published stability claim, and the
rationale for shipping 4 rather than 8. Both anchors that pointed into the
renamed section are updated.

Marked the three benchmark JSONs -diff (5,921 lines). They are NOT dead
evidence: test_phi4_continuation_calibration reads all three at roughly
fifteen sites across six test classes, so the calibrator is tested against
the real measurement rather than invented numbers. Deleting them would
mean gutting a 1,823-line test module. -diff hides them from review and
changes no bytes.

docs/ visible: 8,148 -> 1,222 lines. Branch: 110 files / +49,635 ->
106 files / +42,743.

pytest 312 passed, including the retracted-claims guard that scans the
prose rewritten here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant