[Bugfix][NIXL] Include transfer mode (push/pull) in the compatibility hash (#50620)
Signed-off-by: Tzu-Ling <tzulingk@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tzu-Ling Kan committed
Aug 13, 2026 at 23:11 UTC
653cc6faca6885e36760bf35a25bb63442519b14
8 files changed
+76
-3
docs/features/nixl_connector_compatibility.md
+2
@@ -82,6 +82,8 @@ By default, a **compatibility hash** is checked during handshake. P and D instan
82
- Attention backend
83
- KV cache dtype (`cache_dtype`)
84
- EAGLE/MTP-style speculative method and draft-model configuration
85
+- NIXL transfer mode (push vs pull) — a push (WRITE) connector and a pull (READ)
86
+ connector use incompatible transfer protocols and must never be paired
87
88
!!! warning
89
Disable the hash check with `--kv-transfer-config '{"kv_connector_extra_config": {"enforce_handshake_compat": false}}'` at your own risk.
tests/v1/kv_connector/unit/test_nixl_connector.py
+39
@@ -433,6 +433,9 @@ def test_kv_transfer_handshake(dist_init):
433
)
434
)
435
assert delay
436
+ # Pull connector advertises its transfer mode in kv_transfer_params so
437
+ # an external router can distinguish it from a push producer.
438
+ assert kv_connector_metadata["transfer_mode"] == "pull"
439
440
# Decode connector will be able to create handshake with the prefill connector.
441
decode_connector = NixlConnector(
@@ -2906,6 +2909,42 @@ def test_speculative_attention_backend_not_in_compatibility_hash():
2909
assert local_hash == remote_hash
2910
2911
2912
+@pytest.mark.skip_global_cleanup
2913
+def test_transfer_mode_changes_compatibility_hash():
2914
+ # push (WRITE) and pull (READ) connectors use incompatible transfer
2915
+ # protocols, so their compatibility hashes must differ; identical modes
2916
+ # must match. The default mode is pull.
2917
+ config = create_vllm_config()
2918
+
2919
+ pull_hash = compute_nixl_compatibility_hash(
2920
+ config, "FLASH_ATTN", False, transfer_mode="pull"
2921
+ )
2922
+ push_hash = compute_nixl_compatibility_hash(
2923
+ config, "FLASH_ATTN", False, transfer_mode="push"
2924
+ )
2925
+
2926
+ assert pull_hash != push_hash
2927
+ assert pull_hash == compute_nixl_compatibility_hash(
2928
+ config, "FLASH_ATTN", False, transfer_mode="pull"
2929
+ )
2930
+ assert compute_nixl_compatibility_hash(config, "FLASH_ATTN", False) == pull_hash
2931
+
2932
+
2933
+@pytest.mark.skip_global_cleanup
2934
+def test_scheduler_advertises_transfer_mode():
2935
+ # Each scheduler advertises its transfer mode in kv_transfer_params so an
2936
+ # external router can route pull (READ) vs push (WRITE) producers.
2937
+ from vllm.distributed.kv_transfer.kv_connector.v1.nixl.pull_scheduler import (
2938
+ NixlPullConnectorScheduler,
2939
+ )
2940
+ from vllm.distributed.kv_transfer.kv_connector.v1.nixl.push_scheduler import (
2941
+ NixlPushConnectorScheduler,
2942
+ )
2943
+
2944
+ assert NixlPullConnectorScheduler._TRANSFER_MODE == "pull"
2945
+ assert NixlPushConnectorScheduler._TRANSFER_MODE == "push"
2946
+
2947
+
2948
@pytest.mark.parametrize(
2949
"mismatch_type,config_overrides,version_override,should_fail,enforce_handshake_compat",
2950
[
vllm/distributed/kv_transfer/kv_connector/v1/nixl/base_scheduler.py
+5
@@ -51,6 +51,11 @@ logger = init_logger(__name__)
51
class NixlBaseConnectorScheduler:
52
"""Base implementation of Scheduler side methods shared by pull and push."""
53
54
+ # Emitted in kv_transfer_params so an external router can distinguish a
55
+ # pull (READ) producer from a push (WRITE) one. Overridden by the push
56
+ # scheduler.
57
+ _TRANSFER_MODE: str = "pull"
58
+
59
def __init__(
60
self,
61
vllm_config: "VllmConfig",
vllm/distributed/kv_transfer/kv_connector/v1/nixl/base_worker.py
+10
-1
@@ -90,6 +90,11 @@ logger = init_logger(__name__)
90
class NixlBaseConnectorWorker:
91
"""Base implementation of Worker side methods shared by pull and push."""
92
93
+ # Transfer mode included in the NIXL compatibility hash so that a push
94
+ # (WRITE) connector and a pull (READ) connector never handshake together.
95
+ # Overridden by NixlPushConnectorWorker.
96
+ _TRANSFER_MODE: str = "pull"
97
+
98
def _compute_desc_ids(
99
self,
100
block_ids: BlockIds,
@@ -976,6 +981,7 @@ class NixlBaseConnectorWorker:
981
self.vllm_config,
982
self.backend_name,
983
self.transfer_topo.cross_layers_blocks,
984
+ transfer_mode=self._TRANSFER_MODE,
985
)
986
987
total_size = storage.nbytes()
@@ -1066,7 +1072,10 @@ class NixlBaseConnectorWorker:
1072
is_mamba=self._has_mamba,
1073
)
1074
self.compat_hash = compute_nixl_compatibility_hash(
1069
- self.vllm_config, self.backend_name, self.transfer_topo.cross_layers_blocks
1075
+ self.vllm_config,
1076
+ self.backend_name,
1077
+ self.transfer_topo.cross_layers_blocks,
1078
+ transfer_mode=self._TRANSFER_MODE,
1079
)
1080
1081
if self.use_host_buffer:
vllm/distributed/kv_transfer/kv_connector/v1/nixl/metadata.py
+13
-2
@@ -42,8 +42,9 @@ PUSH_REG_NOTIF_PREFIX = b"PUSH_REG:"
42
# 5: Add remote_blocks_expiry_time to kv_transfer_params + handshake
43
# clock-sync timestamp
44
# 6: Validate EAGLE/MTP speculative configuration compatibility
45
+# 7: Include NIXL transfer mode (push vs pull) in the compatibility hash
46
#
46
-NIXL_CONNECTOR_VERSION: int = 6
47
+NIXL_CONNECTOR_VERSION: int = 7
48
49
50
@dataclass
@@ -123,7 +124,10 @@ def _get_speculative_compatibility_factors(
124
125
126
def compute_nixl_compatibility_hash(
126
- vllm_config: VllmConfig, attn_backend_name: str, cross_layers_blocks: bool
127
+ vllm_config: VllmConfig,
128
+ attn_backend_name: str,
129
+ cross_layers_blocks: bool,
130
+ transfer_mode: str = "pull",
131
) -> str:
132
"""
133
Compute compatibility hash for NIXL KV transfer.
@@ -137,6 +141,11 @@ def compute_nixl_compatibility_hash(
141
- KV cache format (dtype, sliding window)
142
- Attention backend
143
- EAGLE/MTP configuration that affects transferred state
144
+ - Transfer mode (push vs pull)
145
+
146
+ The transfer mode is included because the push (WRITE) and pull (READ)
147
+ connectors use incompatible transfer protocols; a push connector and a
148
+ pull connector must never complete a handshake with each other.
149
150
Note: Factors like tensor_parallel_size, block_size, and kv_cache_layout
151
are validated at runtime in _validate_remote_agent_handshake and are not
@@ -171,6 +180,8 @@ def compute_nixl_compatibility_hash(
180
"cross_layers_blocks": cross_layers_blocks,
181
"is_hma_enabled": is_hma_enabled,
182
"speculative_config": _get_speculative_compatibility_factors(vllm_config),
183
+ # push (WRITE) and pull (READ) connectors are protocol-incompatible
184
+ "transfer_mode": transfer_mode,
185
}
186
187
compat_hash = hash_factors(factors)
vllm/distributed/kv_transfer/kv_connector/v1/nixl/pull_scheduler.py
+1
@@ -277,4 +277,5 @@ class NixlPullConnectorScheduler(NixlBaseConnectorScheduler):
277
tp_size=self.vllm_config.parallel_config.tensor_parallel_size,
278
remote_num_tokens=remote_num_tokens,
279
remote_blocks_expiry_time=blocks_expiry_time,
280
+ transfer_mode=self._TRANSFER_MODE,
281
)
vllm/distributed/kv_transfer/kv_connector/v1/nixl/push_scheduler.py
+3
@@ -60,6 +60,8 @@ class NixlPushConnectorScheduler(NixlBaseConnectorScheduler):
60
hooks.
61
"""
62
63
+ _TRANSFER_MODE: str = "push"
64
+
65
def __init__(
66
self,
67
vllm_config: VllmConfig,
@@ -290,6 +292,7 @@ class NixlPushConnectorScheduler(NixlBaseConnectorScheduler):
292
tp_size=self.vllm_config.parallel_config.tensor_parallel_size,
293
pp_size=self.vllm_config.parallel_config.pipeline_parallel_size,
294
remote_num_tokens=remote_num_tokens,
295
+ transfer_mode=self._TRANSFER_MODE,
296
)
297
298
def build_connector_meta(
vllm/distributed/kv_transfer/kv_connector/v1/nixl/push_worker.py
+3
@@ -77,6 +77,9 @@ _PUSH_WRITER_POLL_INTERVAL_MS = 1.0
77
class NixlPushConnectorWorker(NixlBaseConnectorWorker):
78
"""Push-specific (WRITE) worker logic. See module docstring."""
79
80
+ # Distinguishes push from pull in the NIXL compatibility hash.
81
+ _TRANSFER_MODE: str = "push"
82
+
83
def __init__(
84
self,
85
vllm_config: "VllmConfig",