Skip to content

fix(multimodal): validate NIXL READ metadata before borrowing embedding buffers - #17935

Merged
krishung5 merged 3 commits into
ai-dynamo:mainfrom
glamr-agent:dyn-5224-validate-nixl-read-metadata-before-borrowing-buffers-c59ea2f54d6d
Nov 3, 2026
Merged

krishung5 merged 3 commits into
ai-dynamo:mainfrom
glamr-agent:dyn-5224-validate-nixl-read-metadata-before-borrowing-buffers-c59ea2f54d6d

Conversation

@glamr-agent

@glamr-agent glamr-agent commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Failure behavior

When a worker receives multimodal embeddings over NIXL READ, the receiver takes a buffer from its pre-registered pool before it checks the request. A request that passes schema validation but is not a usable READ then fails after the borrow. One example is metadata with a WRITE operation kind. The buffer never returns to the pool and is never tracked as in use, so it is lost. Its recorded size also keeps the request's size instead of the full buffer size. A valid request larger than the pooled buffer drains the pool the same way.

The defect is in Dynamo. Today the vLLM and common multimodal receivers are built with an empty pool, so every request takes the dynamic path and no deployment is known to have hit this.

Reproduction Without the fix With the fix Note
Exact failure on the reported setup Not run — Production receivers use max_items=0, so they have no pool to drain; the failure needs a pooled receiver, and the ticket reports no deployment that showed it.
Exact failure on a similar setup Not run — Needs a GPU and native NIXL for a live encode-to-prefill embedding transfer; the coding run had neither.
Exact error in a test ❌ Exact error ✅ Passed A real NixlReadEmbeddingReceiver with a one-buffer pool and a mocked NIXL agent, on CPU. Without the fix, a WRITE request raised in begin_read and left the pool empty. Malformed READ requests and an oversized request did the same. With the fix, the pool kept its buffer and original size.

Proposed fix

The receiver now checks the READ contract before it borrows a buffer, with the same checks the multimodal kwargs receiver already runs: READ kind, exactly one remote descriptor, a non-zero pointer, a matching size, a notification key, and agent metadata that decodes to non-empty bytes. These checks also run on the dynamic path. If building the buffer view fails after the borrow, the buffer goes back to the pool with its original size. Once a read has started, nothing changes: a failed read still keeps the buffer out of the pool, because NIXL may still write into it.

Case Before After
WRITE or malformed READ metadata, pooled receiver Error after the borrow; pool loses a buffer Error before the borrow; pool unchanged
Request larger than the pooled buffer Error; pool loses a buffer Error; buffer returned with its original size
Malformed metadata, dynamic path Error after allocating a buffer Error before allocating
Valid request Read succeeds Unchanged
Read fails after it started Buffer kept out of the pool Unchanged

Core change: about 26 lines (+25/−1). The logic is in 1 file, in reading order:

  • about 26 lines in embedding_transfer.py: Checks the READ metadata before a pooled buffer is borrowed. Returns the buffer to the pool when the view setup fails before the read starts.

The rest of the +165/−8 diff is tests, comments, and re-indentation.

Validation

This PR adds 13 unit tests in one new class, TestNixlReadEmbeddingReceiverPreparation, to
components/src/dynamo/common/tests/multimodal/test_embedding_transfer.py. They run
without a GPU, against a mocked native NIXL agent with real CPU buffers.

Behavior checked Checked by Without the fix With the fix
A WRITE or kind-less request is rejected before a buffer is borrowed, and the pool keeps it. Two cases of a new parametrized unit test; also the reproduction above ❌ Failed: begin_read raised after the borrow, and the pool was left empty ✅ Passed
A request with no, or more than one, remote descriptor, a zero pointer, a size mismatch, or an empty notification key is rejected before the borrow. Five cases of a new parametrized unit test ❌ Failed: the pool was left empty, or the connector raised or started a transfer after the borrow ✅ Passed
Empty, non-hex, or non-zlib agent metadata is rejected before the borrow. Three cases of a new parametrized unit test ❌ Failed: pool left empty ✅ Passed
Agent metadata that decompresses to empty bytes is rejected before the borrow. One case of a new parametrized unit test ❌ Failed: the transfer started after the borrow and the pool was left empty ✅ Passed
A valid request larger than the pooled buffer puts the buffer back with its original size. New unit test ❌ Failed: pool left empty ✅ Passed
A buffer whose read has begun is kept out of the pool when the read fails. New unit test ✅ Passed ✅ Passed

Relates to #15482

Closes DYN-5224.

Summary by CodeRabbit

  • Bug Fixes
    • Invalid or incomplete embedding transfer metadata is now rejected before a read begins, helping prevent malformed requests from disrupting transfers.
    • If preparing a tensor view fails, the buffer is restored to the pool so it can be reused.

…ng buffers

NixlReadEmbeddingReceiver.receive_embeddings borrowed a pooled descriptor
before checking that the request described a usable NIXL READ. A malformed
request (for example WRITE metadata, which passes RdmaMetadata validation
but is rejected by begin_read) or a request larger than the pooled buffer
removed the descriptor from the pool and left its _data_size changed.

Check the READ contract (kind, exactly one remote descriptor, non-zero
pointer, matching size, non-empty notification key, decodable agent
metadata) before borrowing, as MmKwargsNixlReceiver does since ai-dynamo#15482.
Return the descriptor with its original size if building the view fails.
Behavior once begin_read is called is unchanged.

Refs DYN-5224

Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
@glamr-agent
glamr-agent requested review from a team as code owners November 3, 2026 08:43
@copy-pr-bot

copy-pr-bot Bot commented Nov 3, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@glamr-agent
glamr-agent temporarily deployed to external_collaborator November 3, 2026 08:43 — with GitHub Actions Inactive
@glamr-agent
glamr-agent temporarily deployed to external_collaborator November 3, 2026 08:43 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

👋 Hi glamr-agent! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added external-contribution Pull request is from an external contributor multimodal fix labels Nov 3, 2026
@glamr-agent

Copy link
Copy Markdown
Collaborator Author

factory: Initial validation snapshot.

Validation status: complete

Check Result Scope or reason
Runs the relevant Python unit tests without requiring a GPU. Passed (exit 0) TestNixlReadEmbeddingReceiverPreparation on CPU with a mocked NIXL agent: 12 passed with the fix. Without the fix, the same tests gave 11 failed, 1 passed. Kept here because CI does not repeat the without-fix comparison.

The real NIXL read on a GPU (TestNixlReadEmbeddingTransfer, marked gpu_1) was not run, because the run had no GPU or native NIXL.

Detailed evidence — validation complete

Left out of the table because CI repeats them: builds the changed Dynamo source and imports its compiled extension (Passed, exit 0); lint and formatting of the changed files (Passed, exit 0).

Evidence tally: [3/3 validated]

Advisory AI assessment: sound

Validation result: pass — the new tests fail on main (11 of 12, with the pool drained) and pass with the fix (12 of 12); the gpu_0 file run (20 passed), lint, and the build/import check pass; only the real-NIXL gpu_1 read is left to GPU CI.

Evidence audit: complete [3/3 validated] — the command report comes from recorded runs.

Commands and results [3/3 validated]

Generated from the commands recorded during this run.

Check 1

Builds the changed Dynamo source and confirms that Python can import its compiled extension.

Result: Passed (exit 0)

Command:

Not shown because the exact command contained private run data.

Check 2

Checks the changed files with the repository's fast lint and formatting commands.

Result: Passed (exit 0)

Command:

bash -c 'set -e; /opt/dynamo/venv/bin/python -m py_compile components/src/dynamo/common/multimodal/embedding_transfer.py components/src/dynamo/common/tests/multimodal/test_embedding_transfer.py && echo py_compile OK; pre-commit run --files components/src/dynamo/common/multimodal/embedding_transfer.py components/src/dynamo/common/tests/multimodal/test_embedding_transfer.py --hook-stage manual'

Check 3

Runs the relevant Python unit tests without requiring a GPU.

Result: Passed (exit 0)

Command:

timeout 600 /opt/dynamo/venv/bin/python -m pytest components/src/dynamo/common/tests/multimodal/test_embedding_transfer.py -k TestNixlReadEmbeddingReceiverPreparation -v -p no:cacheprovider

@glamr-agent

glamr-agent commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

No description provided.

@glamr-agent

Copy link
Copy Markdown
Collaborator Author

factory: @dynamo-ops please run full CI for 481341c

@dynamo-ops

Copy link
Copy Markdown
Contributor

/ok to test 481341c

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Walkthrough

The NIXL read receiver now validates READ metadata before buffer allocation. If tensor-view preparation fails, it restores the borrowed descriptor to the warmed pool. New tests cover invalid metadata, oversized requests, and failed reads.

Changes

NIXL Read Receiver

Layer / File(s) Summary
READ metadata validation
components/src/dynamo/common/multimodal/embedding_transfer.py, components/src/dynamo/common/tests/multimodal/test_embedding_transfer.py
The receiver checks the operation kind, descriptor count and values, notification key, and compressed NIXL metadata before allocating a buffer. Tests verify invalid metadata does not start a read or modify the pooled descriptor.
Descriptor preparation and failure handling
components/src/dynamo/common/multimodal/embedding_transfer.py, components/src/dynamo/common/tests/multimodal/test_embedding_transfer.py
If tensor-view preparation fails, the receiver restores the descriptor’s original size, returns it to the warmed pool, and re-raises the exception. Tests cover an oversized request and a failed read.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: Minimal · up to 48134

Empty decoded metadata may still strand a warmed buffer if NIXL rejects it. This pre-existing edge case does not block merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: validating NIXL READ metadata before borrowing embedding buffers.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description follows the repository's template in substance. It has the Overview content (failure behavior and proposed fix), Key Changes (the before/after table and core change summary), and Valid…
Full details: Description check

Explanation

The description follows the repository's template in substance. It has the Overview content (failure behavior and proposed fix), Key Changes (the before/after table and core change summary), and Validation (a detailed table of 13 unit tests with results with and without the fix). The Issues and Ongoing Work section is covered by the closing references: it says the PR relates to #15482 and closes DYN-5224. Gaps: the template's 'Closes #XXXX' line uses a Linear ticket rather than a GitHub issue number, and the Pull Request Checklist item ('I used Dynamo's /pr-checklist skill') is not shown as checked. The description also has a count mismatch with the PR objectives: the description says 13 unit tests, while the objectives say 12 tests and 11 of 12 failing without the fix. The description's own table also lists 13 tests but the validation text does not reconcile the number. The description includes a 'Core change: about 26 lines (+25/-1)' note while the raw summary reports +32/-8 for the source file and +128/-0 for tests, which is a different accounting, so the line counts should be reconciled. Overall the description is mostly complete, and these are non-critical issues.

Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 3 functions across 2 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between e37f0b9 and 481341c.

Files selected for processing: 2
  • components/src/dynamo/common/multimodal/embedding_transfer.py
  • components/src/dynamo/common/tests/multimodal/test_embedding_transfer.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review.

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix comments: Fix CodeRabbit comments on this PR

…owing

Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
@glamr-agent
glamr-agent temporarily deployed to external_collaborator November 3, 2026 09:01 — with GitHub Actions Inactive
@glamr-agent

Copy link
Copy Markdown
Collaborator Author

factory: @dynamo-ops please run full CI for f6ba3a0

@dynamo-ops

Copy link
Copy Markdown
Contributor

/ok to test f6ba3a0

@devin-ai-integration

Copy link
Copy Markdown
Contributor

✅ Dynamo PR CI passed — run 41607830433 (attempt 1) on f6ba3a064e

Gate checks: ✅ backend-status-check · ✅ deploy-status-check · ✅ dynamo-status-check

Posted automatically by Devin for run 41607830433. Updated on every full-CI run of this PR.

@krishung5
krishung5 merged commit 607f867 into ai-dynamo:main Nov 3, 2026
117 checks passed

This branch was previously deployed

1 inactive deployment
external_collaborator — f6ba3a06 Deployed Nov 3, 2026 by glamr-agent via ok-to-test #44166
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contribution Pull request is from an external contributor fix multimodal size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants