-
Notifications
You must be signed in to change notification settings - Fork 699
feat: nixl_connect: Improve Concurrency Support #4433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This change moves the nixl_connect library from a persistent connector based design to a per connection based design. When an active or passive operation is created, a connection object is created to represent the local side of a connection to a remote worker. The connection is responsible for keeping the descriptor data and operation state separated when multiple operations are executing at the same time. Prior to this change, it was possible for two operations to intersect leading to errors and disconnections. Signed-off-by: J Wyman <jwyman@nvidia.com>
This change updates nixl_connect documentation to reflect the changes in the previous commit. Signed-off-by: J Wyman <jwyman@nvidia.com>
This change updates all of the Dynamo usages of nixl_connect to adopt the changes in the previous commit. - Removal of `Connector.initialize()` calls. - Addition of the `await` keyword to `Connector.create_readable()` and `.create_writable()` calls. Signed-off-by: J Wyman <jwyman@nvidia.com>
cd17235 to
177e283
Compare
WalkthroughThe changes introduce asynchronous operation creation for Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes
Poem
Pre-merge checks❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
lib/bindings/python/src/dynamo/nixl_connect/__init__.py (1)
1739-1791: WriteOperation type check incorrectly expects Connector instead of ConnectionThe new constructor signature is:
def __init__( self, connection: Connection, local_descriptors: Descriptor | list[Descriptor], remote_metadata: RdmaMetadata, ) -> None:But the implementation still does:
if not isinstance(connection, Connector): raise TypeError( "Argument `connector` must be `dynamo.nixl_connect.Connector`." )This is a blocking bug:
- Callers (e.g.,
Connector.begin_write) pass aConnection, not aConnector.- Every valid call will raise
TypeError, preventing write operations from ever starting.- The error message is also now misleading (
connectorvsconnection).The subsequent
Remote(connection, remote_metadata.nixl_metadata)call is otherwise correct, sinceRemoteexpects aConnection.Suggested fix:
- if not isinstance(connection, Connector): - raise TypeError( - "Argument `connector` must be `dynamo.nixl_connect.Connector`." - ) + if not isinstance(connection, Connection): + raise TypeError( + "Argument `connection` must be `dynamo.nixl_connect.Connection`." + )Once updated, the rest of the logic (metadata validation, Remote construction, super call) aligns with the new per-connection model.
docs/api/nixl_connect/read_operation.md (1)
33-44: Doc example for begin_read has argument order reversedThe example currently shows:
with await self.connector.begin_read(descriptor, remote_metadata) as read_op:But the actual signature is:
async def begin_read( self, remote_metadata: RdmaMetadata, local_descriptors: Descriptor | list[Descriptor], ) -> ReadOperation:Passing
descriptorfirst andremote_metadatasecond will fail the type check onremote_metadata. The example should instead be:with await self.connector.begin_read(remote_metadata, descriptor) as read_op: ...so that the
RdmaMetadataand descriptor parameters line up correctly.
🧹 Nitpick comments (6)
lib/bindings/python/src/dynamo/nixl_connect/__init__.py (3)
502-586: Connection abstraction is mostly solid; minor cleanup opportunitiesThe
Connectionclass cleanly encapsulates name, connector, and a dedicatednixl_agent, and the asyncinitializehook gives you room for future setup while staying idempotent.Two small nits:
_agent_metadatais never used; you can drop it or extendmetadatato cache into it if you intend to reuse.- The docstring under
initializestill talks about initializing the “connector”; consider updating to “connection” to avoid confusion.These are cosmetic and can be deferred.
661-745: Async begin_read / begin_write correctly create per-call ConnectionsUsing
await self._create_connection()and passing the resultingConnectionintoReadOperationandWriteOperationachieves the PR’s goal of isolating per-operation NIXL state. The type checks on metadata/descriptors and operation-kind validation are preserved.One inconsistency to consider tightening later:
begin_readcomparesremote_metadata.operation_kindtoOperationKind.READ.value(int), whilebegin_writecompares toOperationKind.WRITE(IntEnum). Both work, but using.valuein both places would be more uniform.
1685-1706: WritableOperation constructor aligns with Connection-based PassiveOperationPassing
connectionintoPassiveOperationfor a WRITE operation is consistent with the per-connection refactor. The docstring still mentionslocal/Connectorin the “Raises” section, which you might want to update toconnection/Connection, but the behavior is correct.docs/api/nixl_connect/write_operation.md (1)
34-45: Example now matches asyncbegin_writeAPIUsing
with await self.connector.begin_write(descriptor, remote_metadata) as write_op:correctly reflects the asyncbegin_writesignature and keeps the lifetime of theWriteOperationscoped to the context manager. If you want slightly clearer style, you could split it intowrite_op = await ...followed bywith write_op:, but the current form is fine.docs/api/nixl_connect/writable_operation.md (1)
34-49: Writable operation example correctly reflects async factoryUsing
with await self.connector.create_writable(descriptor) as write_op:matches the asynccreate_writableAPI and keeps the operation scoped to the context manager while awaiting completion. If desired, you could split the await andwithfor clarity, but the current form is functionally sound.docs/api/nixl_connect/connector.md (1)
108-136: Async factory method docs are aligned; consider clarifying await usageDocumenting
create_readableandcreate_writableasasync defcorrectly reflects their async factory behavior and the per-connection model. To make usage unambiguous for readers skimming this page, consider adding a brief note like “This is a coroutine; use asop = await connector.create_readable(...)” in each section.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
components/src/dynamo/sglang/request_handlers/multimodal/encode_worker_handler.py(1 hunks)components/src/dynamo/sglang/request_handlers/multimodal/worker_handler.py(0 hunks)components/src/dynamo/trtllm/encode_helper.py(1 hunks)components/src/dynamo/trtllm/main.py(0 hunks)components/src/dynamo/vllm/multimodal_handlers/encode_worker_handler.py(1 hunks)components/src/dynamo/vllm/multimodal_handlers/worker_handler.py(0 hunks)docs/api/nixl_connect/connector.md(3 hunks)docs/api/nixl_connect/read_operation.md(1 hunks)docs/api/nixl_connect/readable_operation.md(1 hunks)docs/api/nixl_connect/writable_operation.md(1 hunks)docs/api/nixl_connect/write_operation.md(1 hunks)examples/multimodal/components/encode_worker.py(1 hunks)examples/multimodal/components/video_encode_worker.py(1 hunks)lib/bindings/python/src/dynamo/nixl_connect/__init__.py(41 hunks)
💤 Files with no reviewable changes (3)
- components/src/dynamo/trtllm/main.py
- components/src/dynamo/sglang/request_handlers/multimodal/worker_handler.py
- components/src/dynamo/vllm/multimodal_handlers/worker_handler.py
🧰 Additional context used
🧬 Code graph analysis (5)
examples/multimodal/components/encode_worker.py (1)
lib/bindings/python/src/dynamo/nixl_connect/__init__.py (1)
create_readable(747-761)
components/src/dynamo/vllm/multimodal_handlers/encode_worker_handler.py (1)
lib/bindings/python/src/dynamo/nixl_connect/__init__.py (1)
create_readable(747-761)
components/src/dynamo/sglang/request_handlers/multimodal/encode_worker_handler.py (1)
lib/bindings/python/src/dynamo/nixl_connect/__init__.py (1)
create_readable(747-761)
examples/multimodal/components/video_encode_worker.py (1)
lib/bindings/python/src/dynamo/nixl_connect/__init__.py (1)
create_readable(747-761)
components/src/dynamo/trtllm/encode_helper.py (1)
lib/bindings/python/src/dynamo/nixl_connect/__init__.py (1)
create_readable(747-761)
🪛 Ruff (0.14.5)
lib/bindings/python/src/dynamo/nixl_connect/__init__.py
525-527: Avoid specifying long messages outside the exception class
(TRY003)
529-529: Avoid specifying long messages outside the exception class
(TRY003)
531-531: Avoid specifying long messages outside the exception class
(TRY003)
1024-1026: Avoid specifying long messages outside the exception class
(TRY003)
1028-1028: Avoid specifying long messages outside the exception class
(TRY003)
1034-1037: Avoid specifying long messages outside the exception class
(TRY003)
1550-1552: Avoid specifying long messages outside the exception class
(TRY003)
1554-1554: Avoid specifying long messages outside the exception class
(TRY003)
1556-1556: Avoid specifying long messages outside the exception class
(TRY003)
🔇 Additional comments (23)
lib/bindings/python/src/dynamo/nixl_connect/__init__.py (14)
69-151: Connection-based AbstractOperation wiring looks correctThe move from
Connectorto per-operationConnectionplus registering descriptors againstself._connectionis consistent and isolates state per connection. Argument validation and descriptor tuple creation still enforce the same invariants as before.
224-337: ActiveOperation correctly uses per-connection NIXL agentUsing
remote.connectionto seed the base class and then consistently callingself._connection._nixl.get_xfer_descs/initialize_xferensures the transfer handle is associated with the specific connection instance, which is what you want for concurrent operations. The validation of local/remote descriptors and sizes remains intact.
371-416: Release/cancel paths now route through the Connection’s NIXL agentReleasing the transfer handle and cancelling via
self._connection._nixl.release_xfer_handle(...)is consistent with the new per-connection model and should avoid cross-connection interference, assuming_connectionis always set by construction (which it is viaAbstractOperation).
468-484: Status polling against connection-local NIXL state is consistentSwitching both the initial
transfer(...)call and subsequentcheck_xfer_state(...)toself._connection._nixlkeeps status tracking scoped to the correct agent and avoids global connector races.
617-642: Connector hostname and connection counter plumbing look goodStoring
self._hostnameand exposing it via ahostnameproperty is straightforward and correctly reflected in__repr__. The_connection_countcounter is a simple way to generate unique Connection names per connector instance.
747-795: Connection factory is simple and aligns with the per-connection design
_create_connectionusing a monotonically increasing counter, constructing a newConnection, and awaiting itsinitializemethod is a clean abstraction for the caller methods. The deprecatedinitializeonConnectorbeing a no-op with a log message is a good compatibility story.
859-953: Descriptor destructor correctly deregisters via ConnectionTying
deregister_memorytoself._connection._nixlensures memory is unregistered on the correct agent, and the extra guard on_connection is not Noneavoids dereferencing when a descriptor was never registered. This matches the new per-connection semantics.
1209-1233: PassiveOperation now correctly depends on ConnectionPassing a
Connectiondown intoAbstractOperationand later using it for status/metadata lookups makes the passive side consistent with the active operations. The status initialization and serialization behavior are unchanged.
1275-1303: PassiveOperation.metadata correctly uses connection-level metadataUsing
self._connection.metadataas the source of NIXL agent metadata, compressing with zlib, and then base64/hex encoding is coherent with the new Connection abstraction. The logging around compression ratio is also helpful for diagnosing metadata bloat.
1318-1346: Notification polling via connection-local NIXL agent is appropriateQuerying notifications through
self._connection._nixl.update_notifs()and logging transitions withself._connection.namekeeps status tracking tied to the specific connection/agent and improves observability when multiple connections are active.
1366-1415: ReadOperation now correctly takes a Connection, but remote construction is soundThe updated signature
__init__(self, connection: Connection, ...)with a type check plus instantiatingRemote(connection, remote_metadata.nixl_metadata)is the right way to anchor the remote to a specific connection. The remaining validation and logging mirror the previous behavior.
1469-1475: ReadableOperation’s constructor correctly threads through ConnectionCalling
super().__init__(connection, OperationKind.READ, local_descriptors)cleanly reuses the shared passive logic while binding the operation to a particular connection instance.
1545-1579: Remote now correctly binds to a Connection’s NIXL agentValidating
connectionas aConnection, storing it, and then callingconnection._nixl.add_remote_agent(...)ensures eachRemoteis scoped to the connection’s agent. The fallback decoding logic for legacy metadata remains intact, and__repr__includingconnection={self._connection.name}is useful for debugging.
1602-1619: Remote._release uses connection-local remove_remote_agentCalling
self._connection._nixl.remove_remote_agent(self._name)matches the new ownership model and should prevent stale remote registrations from lingering across operations.components/src/dynamo/vllm/multimodal_handlers/encode_worker_handler.py (2)
66-72: Connector initialization change is compatible with new no-op initialize()Creating
self._connector = connect.Connector()without awaitinginitialize()matches the updated Connector API whereinitializeis deprecated and a no-op. Startup behavior stays simple while still supporting per-requestConnectioncreation viacreate_readable.
125-144: Async readable creation pattern is correct and matches new APIThe block:
descriptor = connect.Descriptor(embeddings_cpu) with await self._connector.create_readable(descriptor) as readable: request.serialized_request = readable.metadata() ... await readable.wait_for_completion()correctly:
- moves embeddings to CPU (to avoid transport issues),
- creates a
Descriptor,- awaits the async factory
create_readable(...)to get aReadableOperation,- then uses the returned object as a synchronous context manager, and
- waits for completion before leaving the context.
This is consistent with
create_readable’s async signature andReadableOperationimplementing a sync context manager.docs/api/nixl_connect/readable_operation.md (1)
33-48: ReadableOperation example correctly reflects async create_readable usageThe example:
with await self.connector.create_readable(descriptor) as read_op: op_metadata = read_op.metadata() ... await read_op.wait_for_completion()matches the updated
Connector.create_readable(...)async API and the fact thatReadableOperationis a synchronous context manager. This should be a good reference for callers adopting the new pattern.components/src/dynamo/sglang/request_handlers/multimodal/encode_worker_handler.py (2)
159-177: SGLang handler correctly adopts async readable creation patternThis section:
descriptor = connect.Descriptor(precomputed_embeddings) with await self._connector.create_readable(descriptor) as readable: request.serialized_request = readable.metadata() ... await readable.wait_for_completion()is consistent with the new async
create_readableAPI and theReadableOperationsynchronous context manager. It cleanly wires the precomputed embeddings into the downstream worker via NIXL metadata and waits for completion before returning.
182-188: Connector initialization update aligns with deprecated initialize()Instantiating
self._connector = connect.Connector()without awaitinginitialize()matches the refactored Connector semantics (per-request connections and deprecated initialize). This simplifies startup for the handler while still enabling concurrent RDMA operations.components/src/dynamo/trtllm/encode_helper.py (1)
242-261: Asynccreate_readableusage is correct and lifecycle-safeAwaiting
connector.create_readable(descriptor)directly in thewithstatement and then awaitingreadable_op.wait_for_completion()inside the block aligns with the new async API and ensures the readable operation (and its underlying connection) is properly cleaned up even if the async generator is cancelled.examples/multimodal/components/encode_worker.py (1)
124-141: Per-request readable creation matches new async connector design
with await self._connector.create_readable(descriptor) as readable:correctly uses the async factory, scopes the readable/connection to the request, and ensuresawait readable.wait_for_completion()runs before responses are streamed. This fits the per-connection concurrency model the PR is introducing.examples/multimodal/components/video_encode_worker.py (1)
153-178: Async readable usage for video path is consistent and correctThe switch to
with await self._connector.create_readable(descriptor) as readable:mirrors the image worker, correctly awaits the async factory, and keeps the RDMA-readable operation alive untilawait readable.wait_for_completion()completes. No issues from a concurrency or lifecycle standpoint.docs/api/nixl_connect/connector.md (1)
153-160:hostnameproperty documentation looks goodThe new
hostnameproperty is clearly described and fits well alongside the other connector properties; it gives users an easy way to introspect the worker host without exposing runtime internals.
ee8731c to
adcf1f4
Compare
Signed-off-by: J Wyman <jwyman@nvidia.com>
adcf1f4 to
2006adc
Compare
This change automatically deregisters registered Descriptor memory when an operation goes out of scope. Deregistering the memory enables the descriptor to be reused with future operations. This is a common design pattern, especially with CUDA device allocated memory. Signed-off-by: J Wyman <jwyman@nvidia.com>
8c88c44 to
d946178
Compare
This change moves the nixl_connect library from a persistent connector based design to a per connection based design. When an active or passive operation is created, a connection object is created to represent the local side of a connection to a remote worker.
The connection is responsible for keeping the descriptor data and operation state separated when multiple operations are executing at the same time.
Prior to this change, it was possible for two operations to intersect leading to errors and disconnections.
Includes documentation changes and updates to dependent EPD code.
Summary by CodeRabbit
New Features
hostnameproperty for accessing connector information.Bug Fixes
Refactor
Connector.initialize()method; initialization now occurs implicitly.namespaceandruntimeproperties from public API.Documentation