Skip to content

fix(agent): collapse duplicate CUDA handle references - #166

Closed
galletas1712 wants to merge 1 commit into
fix/cuinterpose-host-backed-fla-restorefrom
fix/cuinterpose-collapse-alias-references
Closed

galletas1712 wants to merge 1 commit into
fix/cuinterpose-host-backed-fla-restorefrom
fix/cuinterpose-collapse-alias-references

Conversation

@galletas1712

Copy link
Copy Markdown
Contributor

Summary

  • collapse redundant driver references acquired by cuMemRetainAllocationHandle when a tracked allocation already has a live backing handle
  • apply the same one-backing-reference invariant to repeated tracked POSIX imports
  • preserve distinct logical handles while releasing the redundant physical driver reference immediately
  • add a fake-CUDA regression that verifies alias release keeps the backing alive and final release drops it exactly once

Validation

  • make -C agent/cmd/cuinterpose clean test in the CUDA test image
  • go test ./agent/internal/criu ./agent/internal/cuda -count=1
  • live 1 GiB A/B allocation test:
    • before: 1 GiB remained after releasing all three logical aliases
    • after: residual allocation was 0 bytes
  • GLM-5.2-NVFP4 SGLang TP8/EP8, context length 32768, checkpoint captured successfully with all eight GMS devices saving 60,129,542,144 bytes
  • two restored engines completed CUDA restore; active inference returned 42
  • after active-engine failure, the shadow acquired the lock in 135 ms, registered in 4.70 s, and post-failover inference returned 42
  • after active teardown, GPU memory returned exactly to the single-engine baseline

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • main

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 7795f2bf-794e-4960-8ad8-29156fb401db

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@galletas1712
galletas1712 force-pushed the fix/cuinterpose-collapse-alias-references branch from 1f03c19 to 6d1795e Compare September 2, 2026 00:56
@galletas1712
galletas1712 force-pushed the fix/cuinterpose-collapse-alias-references branch from 6d1795e to a5b9f63 Compare September 2, 2026 06:53
@galletas1712
galletas1712 force-pushed the fix/cuinterpose-collapse-alias-references branch from a5b9f63 to 80334de Compare September 2, 2026 07:50
@galletas1712
galletas1712 force-pushed the fix/cuinterpose-collapse-alias-references branch from 80334de to 0e40729 Compare September 2, 2026 08:35
@galletas1712
galletas1712 force-pushed the fix/cuinterpose-collapse-alias-references branch from 0e40729 to 457dc09 Compare September 2, 2026 20:13
@galletas1712
galletas1712 force-pushed the fix/cuinterpose-collapse-alias-references branch from 457dc09 to 0e40729 Compare September 2, 2026 20:20
Signed-off-by: Schwinn Saereesitthipitak <schwinns@nvidia.com>
@galletas1712

Copy link
Copy Markdown
Contributor Author

Superseded by #217 (tracking: one driver handle per allocation, aliases collapsed through the tables). The re-cut stack (#212#220) supersedes this PR. Its body is quoted verbatim in the replacement's Origin section, and each review thread here has a row in the replacement's "Review threads carried" table with what was done about it.

galletas1712 added a commit that referenced this pull request Sep 11, 2026
Turn the forwarding shim into one that knows which CUDA allocations are shared
between processes. Allocations created with exactly the POSIX file-descriptor
handle type are tracked behind logical handles; every other handle type passes
through untouched. Exports return sealed memfd tickets instead of the driver's
descriptor; an import of a ticket asks the creator process for the real
descriptor over its control socket. The creator answers from an export cache
that holds the one real descriptor per allocation, so its listener never calls
into the driver and never takes the shim's main lock, and a creator busy inside
a long collective call cannot stall its peers.

Bookkeeping lives in hash tables plus a sorted range index that shrink as well
as grow; records are freed when no handle and no mapping remains, so a server
that maps and unmaps for days does not accumulate dead entries or slow down.
One driver handle backs each allocation per process: repeated imports and
cuMemRetainAllocationHandle alias it, and the last logical handle releases it.
A ticket lives as long as the creator holds a handle or mapping, so freeing
memory frees it.

cuMemSetAccess keeps the union of access per location so one call per peer GPU
survives; a call that partly overlaps a tracked mapping, or would grant more
than 32 locations, is refused before the driver sees it. cuMemMap refuses
overlapping a tracked mapping and cuMemUnmap may cover several whole mappings
but not cut through one. Imports of descriptors that are not tickets are
counted so the coordinator can refuse to checkpoint while one is alive. Fork
children drop inherited records and register on their first CUDA activity,
including through the driver's own symbol resolution, so a child that
initializes CUDA without VMM calls is still visible.

The multicast wrappers translate tracked member handles for the bind calls and
otherwise still forward. The shim does not yet answer the coordinator's
lifecycle requests; that is the next change.

Compared with the earlier tracking layers (#152, #166): the export happens once
at ticket time instead of on demand under the main lock; access sets are merged
per location instead of overwritten by the last call; lists that never shrank
are replaced by tables that do; the CUDA context is captured at creation rather
than only at export; unmap and map are range-checked; the listener retries
transient accept errors; the socket path is chmod 0600; NULL and non-zero-flag
exports return CUDA_ERROR_INVALID_VALUE as the driver does. Fourteen state
tests, table and export-cache unit tests, and the earlier suites run under
AddressSanitizer and UndefinedBehaviorSanitizer during the agent image build.


Signed-off-by: Schwinn Saereesitthipitak <schwinns@nvidia.com>
galletas1712 added a commit that referenced this pull request Sep 11, 2026
Turn the forwarding shim into one that knows which CUDA allocations are shared
between processes. Allocations created with exactly the POSIX file-descriptor
handle type are tracked behind logical handles; every other handle type passes
through untouched. Exports return sealed memfd tickets instead of the driver's
descriptor; an import of a ticket asks the creator process for the real
descriptor over its control socket. The creator answers from an export cache
that holds the one real descriptor per allocation, so its listener never calls
into the driver and never takes the shim's main lock, and a creator busy inside
a long collective call cannot stall its peers.

Bookkeeping lives in hash tables plus a sorted range index that shrink as well
as grow; records are freed when no handle and no mapping remains, so a server
that maps and unmaps for days does not accumulate dead entries or slow down.
One driver handle backs each allocation per process: repeated imports and
cuMemRetainAllocationHandle alias it, and the last logical handle releases it.
A ticket lives as long as the creator holds a handle or mapping, so freeing
memory frees it.

cuMemSetAccess keeps the union of access per location so one call per peer GPU
survives; a call that partly overlaps a tracked mapping, or would grant more
than 32 locations, is refused before the driver sees it. cuMemMap refuses
overlapping a tracked mapping and cuMemUnmap may cover several whole mappings
but not cut through one. Imports of descriptors that are not tickets are
counted so the coordinator can refuse to checkpoint while one is alive. Fork
children drop inherited records and register on their first CUDA activity,
including through the driver's own symbol resolution, so a child that
initializes CUDA without VMM calls is still visible.

The multicast wrappers translate tracked member handles for the bind calls and
otherwise still forward. The shim does not yet answer the coordinator's
lifecycle requests; that is the next change.

Compared with the earlier tracking layers (#152, #166): the export happens once
at ticket time instead of on demand under the main lock; access sets are merged
per location instead of overwritten by the last call; lists that never shrank
are replaced by tables that do; the CUDA context is captured at creation rather
than only at export; unmap and map are range-checked; the listener retries
transient accept errors; the socket path is chmod 0600; NULL and non-zero-flag
exports return CUDA_ERROR_INVALID_VALUE as the driver does. Fourteen state
tests, table and export-cache unit tests, and the earlier suites run under
AddressSanitizer and UndefinedBehaviorSanitizer during the agent image build.

Signed-off-by: Schwinn Saereesitthipitak <schwinns@nvidia.com>
galletas1712 added a commit that referenced this pull request Sep 11, 2026
Turn the forwarding shim into one that knows which CUDA allocations are shared
between processes. Allocations created with exactly the POSIX file-descriptor
handle type are tracked behind logical handles; every other handle type passes
through untouched. Exports return sealed memfd tickets instead of the driver's
descriptor; an import of a ticket asks the creator process for the real
descriptor over its control socket. The creator answers from an export cache
that holds the one real descriptor per allocation, so its listener never calls
into the driver and never takes the shim's main lock, and a creator busy inside
a long collective call cannot stall its peers.

Bookkeeping lives in hash tables plus a sorted range index that shrink as well
as grow; records are freed when no handle and no mapping remains, so a server
that maps and unmaps for days does not accumulate dead entries or slow down.
One driver handle backs each allocation per process: repeated imports and
cuMemRetainAllocationHandle alias it, and the last logical handle releases it.
A ticket lives as long as the creator holds a handle or mapping, so freeing
memory frees it.

cuMemSetAccess keeps the union of access per location so one call per peer GPU
survives; a call that partly overlaps a tracked mapping, or would grant more
than 32 locations, is refused before the driver sees it. cuMemMap refuses
overlapping a tracked mapping and cuMemUnmap may cover several whole mappings
but not cut through one. Imports of descriptors that are not tickets are
counted so the coordinator can refuse to checkpoint while one is alive. Fork
children drop inherited records and register on their first CUDA activity,
including through the driver's own symbol resolution, so a child that
initializes CUDA without VMM calls is still visible.

The multicast wrappers translate tracked member handles for the bind calls and
otherwise still forward. The shim does not yet answer the coordinator's
lifecycle requests; that is the next change.

Compared with the earlier tracking layers (#152, #166): the export happens once
at ticket time instead of on demand under the main lock; access sets are merged
per location instead of overwritten by the last call; lists that never shrank
are replaced by tables that do; the CUDA context is captured at creation rather
than only at export; unmap and map are range-checked; the listener retries
transient accept errors; the socket path is chmod 0600; NULL and non-zero-flag
exports return CUDA_ERROR_INVALID_VALUE as the driver does. Fourteen state
tests, table and export-cache unit tests, and the earlier suites run under
AddressSanitizer and UndefinedBehaviorSanitizer during the agent image build.

Signed-off-by: Schwinn Saereesitthipitak <schwinns@nvidia.com>
galletas1712 added a commit that referenced this pull request Sep 11, 2026
Turn the forwarding shim into one that knows which CUDA allocations are shared
between processes. Allocations created with exactly the POSIX file-descriptor
handle type are tracked behind logical handles; every other handle type passes
through untouched. Exports return sealed memfd tickets instead of the driver's
descriptor; an import of a ticket asks the creator process for the real
descriptor over its control socket. The creator answers from an export cache
that holds the one real descriptor per allocation, so its listener never calls
into the driver and never takes the shim's main lock, and a creator busy inside
a long collective call cannot stall its peers.

Bookkeeping lives in hash tables plus a sorted range index that shrink as well
as grow; records are freed when no handle and no mapping remains, so a server
that maps and unmaps for days does not accumulate dead entries or slow down.
One driver handle backs each allocation per process: repeated imports and
cuMemRetainAllocationHandle alias it, and the last logical handle releases it.
A ticket lives as long as the creator holds a handle or mapping, so freeing
memory frees it.

cuMemSetAccess keeps the union of access per location so one call per peer GPU
survives; a call that partly overlaps a tracked mapping, or would grant more
than 32 locations, is refused before the driver sees it. cuMemMap refuses
overlapping a tracked mapping and cuMemUnmap may cover several whole mappings
but not cut through one. Imports of descriptors that are not tickets are
counted so the coordinator can refuse to checkpoint while one is alive. Fork
children drop inherited records and register on their first CUDA activity,
including through the driver's own symbol resolution, so a child that
initializes CUDA without VMM calls is still visible.

The multicast wrappers translate tracked member handles for the bind calls and
otherwise still forward. The shim does not yet answer the coordinator's
lifecycle requests; that is the next change.

Compared with the earlier tracking layers (#152, #166): the export happens once
at ticket time instead of on demand under the main lock; access sets are merged
per location instead of overwritten by the last call; lists that never shrank
are replaced by tables that do; the CUDA context is captured at creation rather
than only at export; unmap and map are range-checked; the listener retries
transient accept errors; the socket path is chmod 0600; NULL and non-zero-flag
exports return CUDA_ERROR_INVALID_VALUE as the driver does. Fourteen state
tests, table and export-cache unit tests, and the earlier suites run under
AddressSanitizer and UndefinedBehaviorSanitizer during the agent image build.

Signed-off-by: Schwinn Saereesitthipitak <schwinns@nvidia.com>
galletas1712 added a commit that referenced this pull request Sep 11, 2026
Turn the forwarding shim into one that knows which CUDA allocations are shared
between processes. Allocations created with exactly the POSIX file-descriptor
handle type are tracked behind logical handles; every other handle type passes
through untouched. Exports return sealed memfd tickets instead of the driver's
descriptor; an import of a ticket asks the creator process for the real
descriptor over its control socket. The creator answers from an export cache
that holds the one real descriptor per allocation, so its listener never calls
into the driver and never takes the shim's main lock, and a creator busy inside
a long collective call cannot stall its peers.

Bookkeeping lives in hash tables plus a sorted range index that shrink as well
as grow; records are freed when no handle and no mapping remains, so a server
that maps and unmaps for days does not accumulate dead entries or slow down.
One driver handle backs each allocation per process: repeated imports and
cuMemRetainAllocationHandle alias it, and the last logical handle releases it.
A ticket lives as long as the creator holds a handle or mapping, so freeing
memory frees it.

cuMemSetAccess keeps the union of access per location so one call per peer GPU
survives; a call that partly overlaps a tracked mapping, or would grant more
than 32 locations, is refused before the driver sees it. cuMemMap refuses
overlapping a tracked mapping and cuMemUnmap may cover several whole mappings
but not cut through one. Imports of descriptors that are not tickets are
counted so the coordinator can refuse to checkpoint while one is alive. Fork
children drop inherited records and register on their first CUDA activity,
including through the driver's own symbol resolution, so a child that
initializes CUDA without VMM calls is still visible.

The multicast wrappers translate tracked member handles for the bind calls and
otherwise still forward. The shim does not yet answer the coordinator's
lifecycle requests; that is the next change.

Compared with the earlier tracking layers (#152, #166): the export happens once
at ticket time instead of on demand under the main lock; access sets are merged
per location instead of overwritten by the last call; lists that never shrank
are replaced by tables that do; the CUDA context is captured at creation rather
than only at export; unmap and map are range-checked; the listener retries
transient accept errors; the socket path is chmod 0600; NULL and non-zero-flag
exports return CUDA_ERROR_INVALID_VALUE as the driver does. Fourteen state
tests, table and export-cache unit tests, and the earlier suites run under
AddressSanitizer and UndefinedBehaviorSanitizer during the agent image build.

Signed-off-by: Schwinn Saereesitthipitak <schwinns@nvidia.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