Skip to content

fix(server): price a plan's NPU engines by driver residency - #975

Merged
0xShug0 merged 11 commits into
0xShug0:mainfrom
wOvAN:rknn-guard-engine-residency
Oct 19, 2026
Merged

0xShug0 merged 11 commits into
0xShug0:mainfrom
wOvAN:rknn-guard-engine-residency

Conversation

@wOvAN

@wOvAN wOvAN commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Why

#964 taught the load guard to read the engines an exec_plan declares out of the plan's JSON, and priced them by file size on the stated ground that librknnrt cannot report a weight-set size. It can: rknn_init(..., RKNN_FLAG_COLLECT_MODEL_INFO_ONLY, ...) plus rknn_query(RKNN_QUERY_MEM_SIZE) reports total_weight_size + total_internal_size, which is the allocation the real init goes on to make. That is the instrument #970 put into the shared RKNN layer for the aperture ladder, and the guard is the second consumer it was written for.

The boundary review on this PR moved the plan knowledge out of the server, which is where the shape below comes from.

What

  • IVoiceModelLoader::model_resources(model_path, session_options) (new, default {}) answers what a family will hold resident for one load: a ModelResourcePlan of ModelEngineRequirement{path, resident_mib}, priced by the target runtime, plus resident_cap_mib - the ceiling the family's own eviction rule enforces on that set (0 = no ceiling, the whole set is resident). ModelRegistry::model_resources(family, ...) routes to the loader that owns the family.
  • The plan value is read in one place: runtime::exec_plan_engine_requirements() walks the components' artifact strings, comma-separated ladders included, resolves each name the way a sidecar resolves it (rknn::resolve_engine_path) and prices it with the shared RKNN library (rknn::engine_memory_mib, now declared outside #if defined(ENGINE_HAVE_RKNN) with a stub returning 0, so a consumer in app/server can ask in any build without its own preprocessor test). The spec-backed loader calls it, so a family adopts a residency policy by setting one field of its config (SpecBackedVoiceModelConfig::residency_cap_mib) rather than by teaching a consumer its plan layout.
  • CosyVoice3 is the family with a policy: its flow sidecar evicts rungs to stay inside the NPU aperture, and the ceiling it evicts to moved into rknn_aperture_budget_mib() (declared in rknn_flow.h), the same reading that builds the ladder's ApertureBudget - the number that evicts and the number that estimates cannot drift apart. AUDIOCPP_RKNN_APERTURE_MIB (PR feat(rknn): let the board state its aperture ceiling #979) is read there, so that PR's reader is now subsumed by this one.
  • The server keeps the parts that are its own: engines at 1.0 and not at the runtime factor (a compiled engine is resident at its own allocation; it builds no ggml graph and holds no KV state), the file-size fallback where the runtime cannot answer (any build without an RKNN runtime, any path that is not an engine image), de-duplication against the weight scan, and the family ceiling applied to the priced set. It keeps no plan grammar: an exec_plan value is scanned as a path like any other session option, where the JSON string is never a file, and the engines come from the loader.

A ladder is priced by the sum of its engines, capped by what the family says it keeps. Sum-then-cap is deliberate: the ceiling is measured per board and per engine set (#970), and undercharging is the failure this estimator has already been caught in.

Validation

PC (x86_64, cmake -S . -B build-pc -DENGINE_BUILD_TESTS=ON -DENGINE_BUILD_EXTENDED_TESTS=ON -DAUDIOCPP_MODEL_SET=custom -DAUDIOCPP_MODELS=piper_tts,kokoro_tts,qwen3_tts,voxcpm1,voxcpm2,moss_tts_v15,f5_tts,echo_tts,fish_audio,glm_tts,dots_tts,mira_tts,sanotts,cosyvoice3): server_config_test and rknn_engine_test pass; python3 tools/check_loader_catalog_sync.py passes. server_config_test now pins the new contract: engines of a plan resolved relative to the model directory and priced at 1.0 (not the runtime factor), a family with no ceiling priced by the sum of a ladder, a family that states a ceiling priced by that ceiling (the CosyVoice3 case, ceiling set through its knob: three 1 GiB rungs -> 1024 MiB), an engine that the checkpoint-tree scan already counted not counted twice, an absent engine contributing nothing, and a malformed plan contributing nothing.

Board (Orange Pi 5 Plus, RK3588, 31 GiB, native aarch64 build with -DENGINE_ENABLE_RKNN=ON -DENGINE_RKNN_INCLUDE_DIR=/usr/local/include -DENGINE_RKNN_LIBRARY=/usr/lib/librknnrt.so), one cold audiocpp_server per row, --backend cpu --threads 4 --min-free-memory-mb <n>, one zero_shot request (seed: 1234) against cosyvoice3-q8_0.gguf (2153.1 MiB) with the six-rung exec_plan ladder flow_estimator_seq{110,200,250,400,500,900}.rknn:

/root/pinladder/guard-both.sh <tag> <min_free_mb> <binary> /root/srvconc/server-rknn-full.json
AUDIOCPP_RKNN_APERTURE_MIB --min-free-memory-mb estimate outcome
unset (ladder ceiling 3900) 40000 7.09 GiB rc=503
unset (ladder ceiling 3900) 20000 (28186 MiB available) 7.09 GiB rc=200, WAV a4346f68a776
0 (accounting off) 20000 (28183 MiB available) 13.77 GiB rc=503
0 (accounting off) 40000 13.77 GiB rc=503

Verbatim: cannot load model 'cosyvoice3': estimated 7.09 GiB + 40000 MiB headroom exceeds available host memory (29.76 GiB) and estimated 13.77 GiB + 20000 MiB headroom exceeds available host memory (29.76 GiB). One binary (68b2b589), one config; the ceiling is the only difference between rows, and it flips the guard's decision at the same headroom. The six engines' driver-reported residency sums to 10747 MiB (786/1129/1426/2615/3752/1039) against 4076 MiB of images, so the ceiling is what keeps a 2.1 GiB model loadable on a 31 GiB board.

Audio is unchanged by the patch: the pre-change binary (d982036a, engines priced at file x 1.5) on the same config, same request and --min-free-memory-mb 18000 renders a4346f68a776, bit-identical to the new binary's row above.

Limits

The guard spends about 0.5 s per declared engine at load (rknn_init in info-only mode) - a few seconds once per startup on a six-rung ladder, which is the cost the ladder already pays per cold rung, moved earlier. The guard still only samples host memory; it has never tracked NPU apertures, and it trusts the ceiling the family states, so a family that states none is priced by its sum.

@0xShug0

0xShug0 commented Oct 18, 2026

Copy link
Copy Markdown
Owner

@wOvAN This PR reminds me that I may have overlooked a boundary/ownership issue in #964. The server now parses exec_plan and splits artifact strings to accommodate CosyVoice3’s engine ladder. That knowledge belongs in the model/component, not the server. This PR extends that coupling by adding RKNN driver probing and assuming all declared engines are resident together.

I'd prefer a generic resource-estimation interface: models/components interpret their execution plans and residency policies, the shared RKNN library reports individual engine requirements, and the server only applies its memory guard to the resulting estimate.

I'd hold this PR while we address that boundary, including the existing logic from #964.

@wOvAN

wOvAN commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Reworked in 2dc834a along the boundary you describe, including the #964 logic.

  • IVoiceModelLoader::model_resources(model_path, session_options) (new, default {}) answers what a family will hold resident for one load, as a ModelResourcePlan: the compiled engines its plans declare, each with the resident_mib the target runtime reports, plus resident_cap_mib - the ceiling the family's own eviction rule enforces on that set (0 = the whole set is resident). ModelRegistry::model_resources(family, ...) routes to the loader that owns the family.
  • The plan value is interpreted in one function, runtime::exec_plan_engine_requirements(): it walks the components' artifact strings, resolves each name the way a sidecar resolves it, and prices it through the shared RKNN library (rknn::engine_memory_mib). The spec-backed loader calls it, so a family adopts a residency policy by setting SpecBackedVoiceModelConfig::residency_cap_mib - no consumer learns the plan's layout.
  • CosyVoice3 is the family with a policy, and the ceiling it states is the one the sidecar already enforces: kAperturePinBudgetMiB moved out of the anonymous namespace behind rknn_aperture_budget_mib() (rknn_flow.h), which is what builds the ladder's ApertureBudget and what answers the loader. One reading, so the number that evicts and the number that estimates cannot drift apart.
  • The server keeps pricing, de-duplication and its guard: engines at 1.0 and never at the runtime factor (a compiled engine is resident at its own allocation; it builds no ggml graph and holds no KV state), the file size where the runtime cannot answer, the family ceiling as a cap on the priced set. It keeps no plan grammar at all - an exec_plan value is scanned as a path like any other session option, where the JSON string is never a file and contributes nothing, and the engines come from the loader.

Board evidence for the new shape (RK3588, one cold server per row, six-rung ladder, weights 2153.1 MiB, Σ driver residency 10747 MiB): with the ladder ceiling applied the estimate is 7.09 GiB and with AUDIOCPP_RKNN_APERTURE_MIB=0 it is 13.77 GiB, and at --min-free-memory-mb 20000 with 28186 MiB available that is the difference between rc=200 and rc=503 - estimated 7.09 GiB + 20000 MiB headroom passes, estimated 13.77 GiB + 20000 MiB headroom exceeds available host memory (29.76 GiB) refuses. The rendered WAV is bit-identical to the pre-change binary's on the same request.

AUDIOCPP_RKNN_APERTURE_MIB is #979's knob, and its reader now lives in the public function above, so #979 is subsumed here apart from its docs line and trace. Want it closed with the knob landing in this PR, or kept separate and rebased onto this interface?

@wOvAN
wOvAN force-pushed the rknn-guard-engine-residency branch from 4e00a9d to 241f830 Compare October 18, 2026 22:54
@0xShug0

0xShug0 commented Oct 19, 2026

Copy link
Copy Markdown
Owner

@wOvAN Some issues:

  • Qwen3-TTS loses engine accounting. Its custom loader does not implement the new hook, while the server’s previous scan is removed. Reproduced: a declared decoder contributes nothing beyond the weights-only estimate.

  • The residency cap can underestimate memory. The ladder allows one oversized engine after evicting others. Its sharing budget therefore cannot be used as a hard ceiling on that engine’s allocation.

  • Deduplication discards better estimates. An engine encountered during the file scan is skipped later rather than repriced using driver-reported residency.

  • The boundary remains incomplete. Generic runtime code still interprets RKNN images and comma-separated artifact lists. Models should report their selected resources and residency policy; the server should only account for them.

@wOvAN

wOvAN commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Reworked all four points — force-push set 4f12a323..2d373106.

1. qwen3's custom loader reported nothing. Added a model_resources() override to Qwen3TTSLoader (src/models/qwen3_tts/loader.cpp) that reads qwen3_tts.exec_plan exactly as the spec-backed base does, so a declared decoder is no longer invisible to the guard. Reproduced your repro on the RK3588 board after the fix — a small probe that calls make_default_registry().model_resources(family, path, {family+".exec_plan": plan}) against the shipped stateful vocoder engines (runtime 2.3.2):

$ probe_resources qwen3_tts '{"speech_decoder":{"impl":"rknn","artifacts":{"main":"/root/tts-rknn/vocoder/vocoder_stateful_64b_fp16.rknn","first":"/root/tts-rknn/vocoder/vocoder_stateful_16_fp16.rknn"}}}'
supports_family(qwen3_tts) = true
engines=2 cap_mib=0
  /root/tts-rknn/vocoder/vocoder_stateful_16_fp16.rknn resident_mib=263
  /root/tts-rknn/vocoder/vocoder_stateful_64b_fp16.rknn resident_mib=331

Before the hook the same call returned engines=0. cosyvoice3 reports its ladder (engines=2 cap_mib=3900, 1426 MiB per rung) and piper_tts its vocoder (engines=1, 27 MiB).

2. The cap clipped the set below what the eviction rule admits. Applied budget is now max(largest_single_engine, min(aperture, cap)) — the ladder admits one engine wider than the budget by evicting the rest, so the floor is the widest single rung. The contract is stated in the ModelResourcePlan docblock ("a consumer applying the budget must floor it at the largest single engine…"), and server_config_test covers both sides: a set whose sum fits under the default budget keeps the sum, a 1 GiB ceiling over a 3 GiB rung floors at 3 GiB.

3. Plan engines found by the file scan were priced at the weights proxy. The guard now applies the reported plan first, and the weight/engine file scans skip every path the plan already counted (shared counted set). A re-discovered engine is therefore priced once, at the driver's residency figure, not at weights×1.5. The dedup test now asserts exactly that: with the family compiled, the plan tree prices at weights + engines, the same bytes the scan-only tree re-prices.

4. The generic runtime was interpreting RKNN artifacts. The reader moved out of framework/runtime/options.cpp into framework/rknn as engine::rknn::exec_plan_engine_requirements() (new engine_resources.{h,cpp}, registered in the engine_runtime sources); framework/runtime keeps only the path-free ModelResourcePlan vocabulary. The spec-backed base and the qwen3 loader are its only callers; rknn_engine_test follows the new namespace.

The duplicated budget knob is unified on #979's name: aperture_budget_mib() with the AUDIOCPP_RKNN_APERTURE_MIB env override, one definition in cosyvoice3/rknn_flow.cpp, trace key cosyvoice3.flow.dit.rknn.aperture_budget_mib unchanged.

Validation after the rework: PC ctest 80/80 and check_loader_catalog_sync.py clean; on the board (aarch64, ENGINE_ENABLE_RKNN=ON, ENGINE_BUILD_TESTS=ON -DENGINE_BUILD_EXTENDED_TESTS=ON) full build green, rknn_engine_test and server_config_test both pass against the real librknnrt.so (the plan-reader cases exercise the driver init path, the guard cases the reported-resources path).

@0xShug0

0xShug0 commented Oct 19, 2026

Copy link
Copy Markdown
Owner

@wOvAN Please rebase. It conflicts with the recently merged Clang C++17 fix for #984

@wOvAN
wOvAN force-pushed the rknn-guard-engine-residency branch from 2d37310 to 1014af3 Compare October 19, 2026 17:56
@wOvAN

wOvAN commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main (1014af34, force-push over 2d373106). One conflict, in estimate_model_memory: your #986 C++17 fix rewrote the inline exec_plan scan with [key, value] bindings, and this stack's first commit deletes that scan (the engines move to the loader-reported plan), so the deletion wins — the resulting model_memory.cpp is byte-identical to the pre-rebase review head, the rebase is a pure rebase with no semantic change. Validated after the rebase: check_loader_catalog_sync.py clean; on the RK3588 board (aarch64, ENGINE_ENABLE_RKNN=ON) full rebuild green and rknn_engine_test + server_config_test pass.

@0xShug0

0xShug0 commented Oct 19, 2026

Copy link
Copy Markdown
Owner

@wOvAN I made some changes to this PR directly. Could you test the RKNN part?

Area PR head Changes
Generic loader Automatically scans .exec_plan through RKNN Calls an optional model-owned resource callback
Engine selection Shared reader scans every component/artifact Each model selects its active RKNN component and relevant artifacts
Shared RKNN helper Parses the entire plan, splits artifact lists, and prices engines Receives selected artifact strings; retains shared comma-list parsing, resolution, deduplication, and pricing
Residency policy Server applies the cap and largest-engine floor CosyVoice3 applies those rules and reports the final resident bytes
Server Calculates engine costs and residency policy Excludes reported files from weight scans and adds the final estimate

The current boundary is:

  • Model/component: interprets its exec_plan, selects the active engines, and applies its residency policy. CosyVoice3 owns ladder selection and its eviction budget.
  • Shared RKNN library: parses selected comma-separated engine lists, resolves and deduplicates paths, queries individual residency, and uses file size when driver pricing is unavailable. No component selection or eviction policy. I kept the sharing comma-separated-list parsing as it is more generic.
  • Generic loader: delegates to the model’s optional resource-reporting callback. No RKNN knowledge.
  • Server: receives resource paths and final resident bytes, avoids double-counting files, and applies the memory guard. No model-specific plan or residency-policy logic.

wOvAN and others added 10 commits October 20, 2026 00:17
The load guard estimates a model's footprint from file sizes, so an `exec_plan`
engine is priced at its image. On RK3588 every engine of the CosyVoice3 flow
ladder is a 661-714 MB file that occupies 0.78-3.75 GiB once resident, so the
estimate is wrong per rung in both directions: a 110-frame engine costs less than
1.5x its image, a 250-frame engine 2 GiB more than its image.

Ask the driver instead, through the instrument the aperture ladder already
uses: `engine_memory_mib`, which now has a non-RKNN stub returning 0 so the
declaration is unconditional and a consumer needs no preprocessor test. Where the
driver cannot say -- every build without an RKNN runtime, every file that is not
an engine image -- the engine falls back to its file size, so a PC estimate is
bit-identical to the old one and `server_config_test` passes unchanged.

A ladder is priced by the sum of its declared engines, i.e. as if every rung were
resident. The model evicts to stay inside the NPU aperture, whose ceiling is the
model's policy and not knowable in the server, and undercharging is the failure
this estimate has already been caught in once (0xShug0#964: 3.28 GiB estimated against
6.20 GiB declared).
The load guard read `exec_plan` JSON and split its artifact strings, and priced every
declared engine as resident. Both are the model's knowledge: the plan's grammar belongs to
the family that writes it, and so does the eviction rule that bounds the set (CosyVoice3's
flow ladder shares one NPU aperture and evicts to stay inside it).

`IVoiceModelLoader::model_resources()` now answers what a family will hold resident for one
load -- the engines its plans declare, priced by the target runtime, plus the ceiling its own
policy enforces on that set -- and `ModelRegistry::model_resources()` routes to the loader
that owns the family. The plan value is read in one place, `runtime::exec_plan_engine_requirements()`,
which resolves each artifact the way a sidecar resolves it and prices it with the shared RKNN
library; a spec-backed family adds a residency policy by setting one field of its config.
CosyVoice3 states its aperture ceiling through `rknn_aperture_budget_mib()`, the same reading
that builds the ladder's `ApertureBudget`, so the number that evicts and the number that
estimates cannot drift apart.

The server keeps the parts that are its own: pricing at 1.0 rather than at the runtime factor
(a compiled engine is resident at its own allocation, it builds no ggml graph), the file-size
fallback where the runtime cannot answer, de-duplication against the weight scan, and the
family ceiling applied to the priced set. It keeps no plan grammar: an `exec_plan` value is
scanned as a path like any other option, where the JSON string is never a file, and the
engines come from the loader.
`exec_plan_engine_requirements` is where the plan grammar now lives, and its only coverage so far
is `server_config_test`, which is registered under `ENGINE_BUILD_EXTENDED_TESTS` and so never runs
in CI. The reader's own contract -- a ladder packed into one artifact value, several components, an
artifact that is not a string, an unset ladder, a value that is not JSON -- is the part that needs
no board, so it goes into the RKNN seam gate that CI does run. Prices stay the driver's: in a build
without an RKNN runtime every requirement reports 0, which is what the caller's fallback keys on.
The guard paragraph said the server reads the artifacts out of an `exec_plan`; it now asks the
family's loader for the engines it will hold resident and applies the ceiling that family states.
The `exec_plan` cases asked the guard for engines a plan declares, and the guard asks the loader
that owns the family -- so in a build that compiled neither CosyVoice3 nor Piper, every engine
contributes nothing and the expected numbers were fiction. CI's sanitizer jobs are exactly that
build: `-DAUDIOCPP_MODELS=builtin_audio_utils`, one loader, no `cosyvoice3`. The cases now price
the engines when the family is in this registry and assert the empty plan when it is not, which is
the same contract read from the other side: a family with no loader reports nothing.
…gine

The generic runtime option surface was parsing RKNN execution plans: splitting
comma-separated artifact lists and asking librknnrt what each engine costs.
That is RKNN knowledge living in framework/runtime; move the reader to
engine::rknn::exec_plan_engine_requirements in framework/rknn and have the
spec-backed loader call it. Behavior is unchanged.
qwen3_tts has a custom loader, so it inherited the empty base
model_resources() and a declared RKNN speech decoder contributed nothing to a
consumer's residency estimate. Read qwen3_tts.exec_plan the same way the
spec-backed base does and report its engines priced by the driver.

Validated on RK3588 (ENGINE_ENABLE_RKNN=ON, runtime 2.3.2): registry
model_resources("qwen3_tts", ..., exec_plan of the two stateful vocoder
engines) reports engines=2 with resident_mib=263/331 from RKNN_QUERY_MEM_SIZE,
where the same call before this hook reported engines=0.
The residency budget accessor was added twice under two names across the
stacked branches (rknn_aperture_budget_mib here, aperture_budget_mib with the
AUDIOCPP_RKNN_APERTURE_MIB env knob there). Keep the latter, which the trace
key already follows, so one knob exists after both land.
…gine

Three changes to the load guard's estimate, from the review:

- Apply the loader-reported resources before the weight-file scans and skip
  counted paths, so a declared engine found by both paths is priced once and at
  the driver's residency figure, not at the weights x1.5 proxy.
- When a family reports a sharing budget, floor the applied cap at the largest
  single engine: the ladder's eviction rule admits one engine wider than the
  budget by evicting the rest, so min(aperture, cap) under-counted a set whose
  single widest rung exceeds the budget.
- Extend the model_resources docblock with that floor contract, and document
  the reported-first, budget-floored flow in the server README.

Tests cover the floor (sum fits under the default budget; a ceiling floors at
the largest rung), the de-duplication (a plan engine and its file scan price
once, at the engine figure when the family is compiled), and the qwen3 hook.
@wOvAN
wOvAN force-pushed the rknn-guard-engine-residency branch from 1743d57 to 8f64190 Compare October 19, 2026 19:24
@wOvAN

wOvAN commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Tested your rewrite on the RK3588 board (aarch64, ENGINE_ENABLE_RKNN=ON, runtime 2.3.2). Rebased the whole stack onto 4faef969 (post #985/#989/#990) and force-pushed 1743d57d..8f641903; one fix went into the rebase: rknn_engine_test had both my exec_plan reader case and #989's driver cases, and your rewrite replaces the reader, so the reader case is now a rknn::engine_resources case (comma-ladder split, resolution, dedup, missing-file skip, file-size fallback, aggregate) on top of #989's magic-fixture and driver-report cases.

Unit tests on the board: rknn_engine_test, server_config_test, rknn_sidecar_test all pass; PC build + full ctest 80/80.

registry.model_resources() probes against real engines (driver-priced, policy folded in as you specified):

case result
qwen3, plan main+first vocoders engines=2 resident_bytes=622854144 (331 + 263 MiB)
cosyvoice3, ladder 110+200 (sum 1915 MiB < budget 3900) resident_bytes=2008023040 = the sum
cosyvoice3, ladder 250+400 (sum 4041 > budget) resident_bytes=4089446400 = exactly 3900 MiB, the floor works
cosyvoice3, ladder 110+200 with AUDIOCPP_RKNN_APERTURE_MIB=1024 resident_bytes=1183842304 = 1129 MiB = largest single rung, floors correctly
piper, plan main w93 engine engines=1 resident_bytes=28311552 (27 MiB)
qwen3, impl: ggml engines=0

One real finding: a malformed plan now crashes the server at startup when the guard is on. The four model_resources callbacks call io::json::parse (and require_string(artifacts, "main")) uncaught, and runtime.cpp:2352 calls estimate_model_memory_bytes outside any handler. Measured on the board: --config with "cosyvoice3.exec_plan": "{not json" and --min-free-memory-mb 1 → the process dies before serving, audiocpp_server failed: failed to parse json at byte 2. The pre-rewrite contract was explicit that the guard stays silent on a malformed plan and the session raises it on load with the component vocabulary; as merged, one typo in one model's plan turns into a whole-server startup failure, and it also fires for a plan that is valid JSON but lacks the selected artifact key.

Minimal fix along your boundary: try { parse } catch → return {} in each callback (the guard keeps its "report nothing, let the load diagnose" rule; sessions still raise on load with their own messages). Happy to push it as a follow-up commit if you want it in this PR.

@0xShug0

0xShug0 commented Oct 19, 2026

Copy link
Copy Markdown
Owner

@wOvAN Thanks for testing! I will impl the fix.

@0xShug0

0xShug0 commented Oct 19, 2026

Copy link
Copy Markdown
Owner

PR updated.

@wOvAN

wOvAN commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Tested 36d67745 on the RK3588 board (full rebuild, 0 errors; rknn_engine_test, server_config_test, rknn_sidecar_test all pass — 3/3).

The deferred-error path is what I re-ran the probe suite against, calling registry.model_resources directly per family:

  • Malformed plan, qwen3 ({not json): engines=0 resident_bytes=0 — no throw. Before the fix this was failed to parse json at byte 2 escaping the guard.
  • Plan without main, piper ({"decoder":{"impl":"rknn","artifacts":{}}}): engines=0 resident_bytes=0 — no throw; previously missing required json key: main.
  • Good plans are unchanged, which was the risk in the same callbacks: qwen3 speech_decoder on vocoder_stateful_64b_fp16.rknn still prices at the driver's residency (347078656 = 331 MiB), cosyvoice3's 250+400 ladder still floors to the 3900 MiB aperture budget (4089446400), piper's w93 decoder still reports 27 MiB.

And end to end on the server binary from the same rebuild: the config that killed the previous head at startup (exec_plan set to a non-JSON string + --min-free-memory-mb 1, so the guard is the first thing to touch the plan) now starts and stays alive. The session-level contract holds — the server boots, and the invalid plan raises only when a session loads.

LGTM from my side.

@0xShug0
0xShug0 merged commit c6abb44 into 0xShug0:main Oct 19, 2026
12 checks passed
cunba-ai pushed a commit to cunba-ai/audio.cpp that referenced this pull request Oct 28, 2026
… Studio polish + Clang CI + prebuilt RK3588 engine packages

Upstream commits fcb4619..503dfb3 (11):
- LoRA adapter refactor + ComfyUI/LoKr for YuE2, VibeVoice, ACE-Step (0xShug0#974)
- Studio layout polish, unified Studio/Arena voice selection (0xShug0#980)
- C++17 structured-binding capture fix (0xShug0#986)
- refactor(rknn): sliced graph through one shared helper (0xShug0#983)
- Linux Clang C++17 CI build (0xShug0#987)
- voxcpm2: cheaper AudioVAE decode (0xShug0#981)
- refactor(tools/rknn): one report line/bootstrap per converter (0xShug0#990)
- Higgs Audio voice preserved across reference-free text chunks (0xShug0#985)
- fix(rknn): driver named in sidecar failure (0xShug0#989)
- server: NPU engines priced by driver residency (0xShug0#975)
- feat(model_spec): prebuilt RK3588 engines as rknn packages (0xShug0#991)

# Conflicts:
#	CMakeLists.txt
#	src/models/higgs_audio_tts/session.cpp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants