fix(LOAD-MODELOPT-NVFP4-BORROW): one owner per device resident - #3355
Merged
mudler-agent merged 2 commits intoOct 2, 2026
Merged
Conversation
added 2 commits
September 29, 2026 20:41
…t leak The fp4 resident publishes one device block on two owning handles, and every Marlin repack builder released only one of them, so each repacked weight kept a full packed+scale device copy alive for the process lifetime. Record the defect, the single-owner remedy, the red-first test that proves the release frees, and the checker clause the new contract needs. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:opencode-go/deepseek-v4.1-flash [pi]
Nvfp4Weight published one allocation on two owning handles: its d_packed/d_scale and the packed/scale d_dev alias that AdoptDeviceBytesAsHost keys on. Releasing a resident meant dropping both, and every Marlin repack builder dropped only d_packed — so each repacked weight (and each expert on MoE models) leaked a full packed+scale device copy for the process lifetime. d_dev is already the generic raw-twin slot the residency machinery keys on, so the type-specific handles are redundant. ResidentNvfp4 uploads into packed.d_dev/scale.d_dev directly and ReleaseResident() is the single release; it drops an adopted host view first, since that view holds the same block alive on host-addressable devices. Behavior is unchanged: same bytes, same adoption, same release points. The new case in test_load_direct_upload pins the release the repack builders perform: RED before this change (CHECK(b.frees == 2) read 0, the alias kept the blocks), GREEN after; 17/17 cases, 206 assertions. scripts/check-fp4-resident-consistency.py pinned the old two-handle contract and would have failed on this tree. Its publication clause now requires a backend-owned allocation, and a new clause requires ReleaseResident() to reset BOTH d_dev slots, dropping an adopted twin view before each reset. Its mutation suite gains the release cases. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:opencode-go/deepseek-v4.1-flash [pi]
6 tasks done
iiLaurens
marked this pull request as ready for review
September 29, 2026 21:36
This was referenced Sep 29, 2026
Contributor
Author
|
Human comment: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Row
LOAD-MODELOPT-NVFP4-BORROW— one row per PR. Spec:.agents/specs/nvfp4-one-owner-per-resident.md.Before starting
borrow) is a different defect in the same row and closed in flow. Filed
ISSUE-LOCAL-01M3QDT6A8JEM8WNJTPR16MXHR.implementation.
LOAD-MODELOPT-NVFP4-BORROWin.agents/engine-matrix.md; the row specload-modelopt-nvfp4-borrow.mdownsResidentNvfp4's upload and adoption path.Nvfp4Weight(qwen3_5_weights.h:672-712),OwnedTensor::d_dev(
:195), bothResidentNvfp4copies (dense_nvfp4_gemm.h:319,qwen3_5.cpp:1449),and every release site (
dense_nvfp4_gemm.h:448,667;qwen3_5.cpp:2952,3132,6890;laguna.cpp:650).What changed
Nvfp4Weightpublished one device allocation on two owning handles: thetype-specific
d_packed/d_scaleand the genericpacked.d_dev/scale.d_devslot that
AdoptDeviceBytesAsHostkeys on. Every Marlin repack builder reset onlythe type-specific pair, so the
d_devalias kept a full packed+scale device copyalive for the process lifetime — once per repacked weight, and once per expert per
projection on an MoE model. That undoes the repack's whole point ("then FREE the
fp4 originals") on exactly the 24 GiB cards the NVFP4 arms serve from.
The change makes
d_devthe one owner. The type-specific members are deleted (aremaining reader fails to compile), both upload copies publish into
d_devdirectly, and
Nvfp4Weight::ReleaseResident()is the single release: it drops anadopted host twin view first (that view holds the same block on a host-addressable
device), then resets both slots.
scripts/check-fp4-resident-consistency.py, whichpinned the old two-handle contract, is updated in the same change: its publication
clause now requires a backend-owned allocation and a new clause requires the
release to reset BOTH slots with the twin drop first.
Evidence
RED, before the fix (the new case, against unmodified
main):The invariant is that a release through the weight's own resident state frees both
buffers. Pre-fix it freed zero: the surviving
packed.d_devalias owned the block.GREEN, after the fix:
Checker, including its mutation suite:
The new release clause goes red under each live-tree mutation of
qwen3_5_weights.h: droppacked.d_dev.reset(), dropscale.d_dev.reset(), dropeither adopted-twin drop, move a twin drop after its reset; and the publication
clause under
= nullptror a bare alias of another object'sd_dev.python3 scripts/check-agent-record.pyOK; commit style and trailercontracts OK for both commits
tests/vllm/test_load_direct_upload.cpp,tests/scripts/test_check_fp4_resident_consistency.pyctest --test-dir build: 795 of 809 pass, and every one of the 14failures reproduces on a clean
origin/main(b45a94273) build:test_model_loader_gguf,test_gliner2_e2e,test_gdn_v_head_permute,test_deepseek_v4_exl3_loader,test_qwen4_exp_layer_loop,test_glm4_moe_lite_paged_engine,test_bench,test_bench_kv_cache_dtype,test_bench_eos_chat_template,test_capi,test_qwen3_dense_async_serving,test_qwen3_paged_engine(each run as abase binary, exit 1), plus
test_cpu_threadpool(segfault/abort 3/3 onbase) and
test_serve_low_tools(fails on base).test_serve_deepseek_v4_mmhangs in
accept()on this box and isNot Runin the base ctest.scripts/agent-preflight.sh— RED ON THIS BOX AT BASE. Each failingchecker was re-run on a clean
origin/mainworktree and fails there too;none of the 35 red gates touches a file this PR changes.
End-to-end A/B on the official NVFP4 safetensors checkpoint
The unit gate proves the release frees; this proves the release MATTERS on a real
artifact.
ukisai/Swift-1.5-Qwen3.8-27b-NVFP4(21.94 GB, 5 shards, public andungated), the ModelOpt NVFP4 + FP8 checkpoint the local GGUF is converted from,
run on the local
sm_120acard (RTX PRO 4000 Blackwell, 24 GiB) under the filemutex, same command, same binary build recipe, only the branch differs:
" Paris.\nThe capital of Germany is"), 1.064 tok/s,device_upload=12.044 GiBmain+ the unrelated PermuteVHeads CUDA kernel (PR #3359)engine-fatal: EngineCore busy loop threw: vt cuda: marlin_repack: malloc bqt: out of memoryatdevice_upload=8.619 GiBmain+ the fp8-KV prefill change (PR #3360)device_upload=8.619 GiBTwo independent trees without the fix die in the NVFP4 Marlin repack; the fix
reaches the same repack and finishes. That is the leak:
BuildMarlinDenseResidentrepacks a weight and frees the fp4 originals only through the type-specific handle,
and the surviving
d_devalias keeps a full packed+scale copy alive per repackedweight — so the first forward runs out of card. The repack's own comment says the
free is the point ("we do this repack lazily on first forward and then FREE the
fp4 originals"), and on a 24 GiB card a second copy is the difference between
serving and OOM.
Speed claims
taken and none is implied.
Honest gaps
through the fake backend; it does not measure a repacking serve's process RSS.
still owes its own mutation pass.
scripts/agent-preflight.shon this box is red at base (35 gates): recorddrift in
.agents/completed/anddocs/benchmarks/, supported-models drift,the oracle pin mismatch (
e126687a9avsa7c23ac96dinupstream-sync.md),and one environment FAIL (
fileis not installed for the release-archivetest). Every one reproduces on clean
main, and none touches a changed file.Refs ISSUE-LOCAL-01M3QDT6A8JEM8WNJTPR16MXHR
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:opencode-go/deepseek-v4.1-flash [pi]