Skip to content

feat(memory): pin the provider/context across a plugin's deferred release - #1725

Merged
justinchuby merged 2 commits into
mainfrom
squad/1186-provider-context-pinning
Aug 22, 2026
Merged

justinchuby merged 2 commits into
mainfrom
squad/1186-provider-context-pinning

Conversation

@justinchuby

Copy link
Copy Markdown
Owner

Closes the back half of #1186's Phase 6 criterion 8:

Deferred release keeps the plugin module and provider/context pinned until callbacks and frees retire.

The module half was already enforced. The provider half was not, and docs/memory/MEMORY_PLUGIN_ABI.md said why in as many words: "No execution provider is wired to this ABI in this phase, so … has nothing to pin."

Why this needs a structural gate

A stream-ordered free retires against whoever owns the device resources. If that owner finished teardown first, the plugin unmaps handles the teardown already released.

Nothing on the Rust side reports this. host_release_completed touches only HostBridge — a mutex, some atomics — and that block is deliberately built to outlive the allocator (AllocatorCore::drop mem::forgets it when releases are outstanding, so a plugin thread cannot write into freed memory). So the host stays quiet while the driver is handed a double free. There is nothing to observe after the fact, which is why the gate is on the way in rather than on the completion.

What changed

  • memory-api gains ProviderContextPin / ProviderContextPinSource. Object-safe, because onnx-runtime-memory-host cannot depend on the governor.
  • The governor implements them over ContextRecord::begin_transaction. A plugin release becomes one entry in the same transaction count remove_provider_context already waits on, rather than a second mechanism that could disagree with the first.
  • PluginFactory::open_with_provider_context binds a context. enqueue_release takes a pin before any counter moves, and refuses outright once a context stops accepting work — the allocation stays live so the canonical path can still take it. Queuing unpinned would be the same defect with a narrower window.
  • The pin lives in HostBridge, not AllocatorCore, for the same reason module does: AllocatorCore::drop leaks that block precisely when releases are outstanding, so a pin dying with the allocator would stop guarding the exact window it exists for.
  • open() is unchanged and stays the standalone case. A mechanism with no provider behind it has no context to outlive, so requiring one would be a regression rather than a tightening.

Verification — injection, not a first green run

The path this guards was unreachable before, so any implementation goes green on the first try. With pin acquisition disabled (&& false):

a_queued_release_blocks_provider_context_teardown_until_it_retires ... FAILED
  provider-context teardown returned while a deferred release was still outstanding
a_retiring_provider_context_refuses_further_deferred_releases ... FAILED
test result: FAILED. 2 passed; 2 failed

Restored → 4 passed. The two control tests stay green in both runs, which is what makes the block attributable to the pin rather than to wait_quiescent being slow in general:

  • teardown with no queued release is not delayed;
  • an unbound allocator still queues and drains normally.

The blocking test also asserts teardown resumes after the drain. Asserting only that it blocks would pass for a deadlock — a pin that never releases reads as a successful pin.

Local gates: cargo fmt --all -- --check clean; clippy --all-targets -- -D warnings on the three crates exit 0; memory-api / memory-governor / memory-host / ep-api / session all green, 0 failed.

What this does not do

No in-tree execution provider opens a plugin allocator yet, so nothing in production passes a pin source today — onnx-runtime-memory-host still has no dependents. The mechanism and its guarantees exist and are tested; adopting them from the CUDA EP is the remaining half of memory-plugin-provider-wiring and needs GPU verification I cannot do locally. Recorded in the ABI doc rather than claimed as done.

#1186 should stay open until that adoption lands. Checkbox audit for the rest of the issue is in this comment (74/100 ticked).

…ease

Closes the back half of #1186's Phase 6 criterion 8. The module half was
already enforced; the provider/context half had nothing to attach to.

A stream-ordered free retires against whoever owns the device resources. If
that owner finished teardown first, the plugin unmaps handles the teardown
already released. Nothing on the Rust side reports this: release_completed
touches only host state, which is deliberately built to outlive the allocator,
so the host stays quiet while the driver is handed a double free. That silence
is why the gate is on the way in rather than on the completion.

- memory-api gains two object-safe traits so the plugin host can hold a pin
  without depending on the governor.
- The governor implements them over ContextRecord::begin_transaction, so a
  plugin release is one entry in the same count remove_provider_context
  already waits on rather than a parallel mechanism.
- PluginFactory::open_with_provider_context binds a context; enqueue_release
  takes a pin before any counter moves and refuses outright once a context
  stops accepting work, leaving the allocation live for the canonical path.
  The pin lives in HostBridge, not AllocatorCore, because AllocatorCore::drop
  deliberately leaks that block precisely when releases are outstanding.
- open() stays the standalone case: a mechanism with no provider behind it has
  no context to outlive.

Verified by injection, not by a first green run. With pin acquisition disabled,
teardown returns while a release is outstanding and a retiring context accepts
the enqueue; both restored tests pass, and the two control tests stay green
either way so the block is attributable to the pin.

Not wired: no in-tree execution provider opens a plugin allocator yet, so
nothing in production passes a pin source. Recorded in the ABI doc.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c80f8522-983c-47f7-8241-2155a823aabe
@codecov

codecov Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.69014% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.43%. Comparing base (7af550a) to head (6af8b25).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
crates/onnx-runtime-memory-governor/src/manager.rs 60.86% 9 Missing ⚠️
crates/onnx-runtime-memory-api/src/context_pin.rs 71.42% 2 Missing ⚠️
crates/onnx-runtime-memory-host/src/allocator.rs 93.75% 0 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1725      +/-   ##
==========================================
+ Coverage   80.03%   80.43%   +0.39%     
==========================================
  Files         410      411       +1     
  Lines      198269   198336      +67     
  Branches   198269   198336      +67     
==========================================
+ Hits       158685   159529     +844     
+ Misses      34195    33413     -782     
- Partials     5389     5394       +5     
Flag Coverage Δ
cli-ort-linux 72.47% <ø> (ø)
cli-ort-windows 72.06% <ø> (+0.09%) ⬆️
mlas 85.33% <ø> (+0.13%) ⬆️
offline 80.57% <81.69%> (+0.41%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
crates/onnx-runtime-memory-api/src/lib.rs 100.00% <ø> (ø)
crates/onnx-runtime-memory-host/src/loader.rs 88.60% <100.00%> (+0.21%) ⬆️
crates/onnx-runtime-memory-api/src/context_pin.rs 71.42% <71.42%> (ø)
crates/onnx-runtime-memory-host/src/allocator.rs 71.44% <93.75%> (+0.64%) ⬆️
crates/onnx-runtime-memory-governor/src/manager.rs 75.77% <60.86%> (+0.42%) ⬆️

... and 9 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@justinchuby
justinchuby merged commit ab0880b into main Aug 22, 2026
15 of 19 checks passed
@justinchuby
justinchuby deleted the squad/1186-provider-context-pinning branch August 22, 2026 16:47
justinchuby added a commit that referenced this pull request Aug 22, 2026
…1762)

**`main` does not compile in the CUDA lane, and my own #1725 is what
broke it.** Flagging that first because it gates every PR currently
open.

```
error[E0599]: no method named `device` found for struct `RegisteredMemoryContext`
  --> crates/onnx-runtime-ep-cuda/src/provider.rs:1163:30
error: could not compile `onnx-runtime-ep-cuda` (lib)
exit=101
```

First red at `a2cd1e7bf`; `a2a61656f` immediately before it is green.
`git show ab0880b` (#1725) contains the removal.

## What happened

#1725 rewrote the `use` list in `manager.rs` to pull in the pin-source
types, and `pub fn device` was deleted along with it. Nothing in that PR
required its removal — the underlying
`RegisteredProviderContext::device` is untouched — so this restores the
forwarder verbatim.

## Why nothing caught it, which is the part worth keeping

Three independent things had to line up, and each is individually
reasonable:

1. **`onnx-runtime-ep-cuda` has exactly one caller of this method, and
it compiles in exactly one job.** Every lane that ran on #1725 was blind
to it by construction.
2. **I verified #1725 with `cargo clippy --all-targets -- -D warnings`
on the three crates I had edited.** That is a real check, and it is
exactly the crate set that cannot observe this break. Editing crate A
and verifying crate A is the natural thing to do and it is not
sufficient when A is a library.
3. **A `pub` method with no in-crate caller draws no `dead_code`
warning.** So deleting it was silent going out, and the breakage was
silent coming back — until a job that most PRs never reach.

This is the same shape as the `kv_commits_on_demand` finding in
#1631/#1632: several individually-sound reasons not to look, converging.
I had that lesson in hand and still shipped this, so the lesson
evidently was not "run clippy" — it was **verify the reverse
dependencies of a library change, not the library.**

## Verification — the lane's own command, not a proxy

```
cargo check -p onnx-runtime-ep-cuda -p onnx-runtime-python --features onnx-runtime-python/cuda

main  -> exit 101 (the E0599 above)
fixed -> exit 0
```

Negative control taken deliberately: I stashed the fix and re-ran on
clean `main` to confirm the error returns, so this is demonstrably the
cause rather than a change that merely happens to compile.

Both results reproduced **locally on macOS aarch64 with no CUDA toolkit
and no GPU.** That matters — this was catchable on any developer
machine, which means it was not a CUDA-hardware coverage gap.

Also clean: `cargo fmt --all -- --check`; `clippy -D warnings` on the
governor; 67 + 34 + 5 governor tests; and the native-CUDA clippy lane
from #1632.

## Not fixed here

`Mobius metadata packages` is also red, with `The process '/usr/bin/git'
failed with exit code 1` and no compile error. Different failure,
unrelated to this change, and I have not diagnosed it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c80f8522-983c-47f7-8241-2155a823aabe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant