Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
fbc3651
init example
tedzhouhk Feb 21, 2025
9299991
add nixl to dockerfile.vllm
tedzhouhk Feb 21, 2025
7206489
add nixl torch example
tedzhouhk Feb 21, 2025
d7c607d
wip vllm with nixl
tedzhouhk Feb 22, 2025
80dfe9e
first working nixl conditional prefill
tedzhouhk Feb 22, 2025
ef012e0
add readme
tedzhouhk Feb 22, 2025
3094654
use callback for remote prefill req
tedzhouhk Feb 22, 2025
6078cdf
wip tp > 1
tedzhouhk Feb 23, 2025
88f3d87
add nixl metadta struct
tedzhouhk Feb 23, 2025
6f7e4ce
update readme with tp > 1
tedzhouhk Feb 23, 2025
be6588d
decode run with MQLLMEngine
tedzhouhk Feb 24, 2025
21f33ca
decode on triton
tedzhouhk Feb 24, 2025
00ffc97
triton dummy prefill
tedzhouhk Feb 24, 2025
41a16a2
triton prefill
tedzhouhk Feb 24, 2025
cda0077
update readme
tedzhouhk Feb 24, 2025
1971de5
remove nixl torch example
tedzhouhk Feb 24, 2025
6eb260d
update todos
tedzhouhk Feb 24, 2025
85ef7f5
update todos
tedzhouhk Feb 24, 2025
e8434f6
add http endpoint
tedzhouhk Feb 24, 2025
81ab078
remove remote prefill response
tedzhouhk Feb 25, 2025
899cc75
update todos
tedzhouhk Feb 25, 2025
be2682d
exchange metadta over fs
tedzhouhk Feb 25, 2025
c0e2357
do not restrict mem
tedzhouhk Feb 25, 2025
3b42c68
update readme
tedzhouhk Feb 25, 2025
d80e1d6
init example
ptarasiewiczNV Feb 21, 2025
fb357ca
add nixl to dockerfile.vllm
ptarasiewiczNV Feb 21, 2025
e51ff32
add nixl torch example
ptarasiewiczNV Feb 21, 2025
d6fba17
wip vllm with nixl
ptarasiewiczNV Feb 22, 2025
f959b01
first working nixl conditional prefill
ptarasiewiczNV Feb 22, 2025
0ff5e41
add readme
ptarasiewiczNV Feb 22, 2025
c93a152
use callback for remote prefill req
ptarasiewiczNV Feb 22, 2025
7c8f728
wip tp > 1
ptarasiewiczNV Feb 23, 2025
1fbc202
add nixl metadta struct
ptarasiewiczNV Feb 23, 2025
2fe0710
update readme with tp > 1
ptarasiewiczNV Feb 23, 2025
d976011
decode run with MQLLMEngine
ptarasiewiczNV Feb 24, 2025
be5cd23
decode on triton
ptarasiewiczNV Feb 24, 2025
96cde49
triton dummy prefill
ptarasiewiczNV Feb 24, 2025
1438fb4
triton prefill
ptarasiewiczNV Feb 24, 2025
40959d7
update readme
ptarasiewiczNV Feb 24, 2025
594161d
remove nixl torch example
ptarasiewiczNV Feb 24, 2025
d72fc11
update todos
ptarasiewiczNV Feb 24, 2025
350b831
update todos
ptarasiewiczNV Feb 24, 2025
07c00a3
add http endpoint
ptarasiewiczNV Feb 24, 2025
46ecc50
remove remote prefill response
ptarasiewiczNV Feb 25, 2025
2edc85d
update todos
ptarasiewiczNV Feb 25, 2025
8624ac5
exchange metadta over fs
ptarasiewiczNV Feb 25, 2025
0a76d5e
do not restrict mem
ptarasiewiczNV Feb 25, 2025
9d33228
update readme
ptarasiewiczNV Feb 25, 2025
78615eb
update dockerfile with nixl changes
ptarasiewiczNV Feb 26, 2025
4d6088a
update patch
ptarasiewiczNV Feb 26, 2025
e46aeda
Merge branch 'main' of github.com:triton-inference-server/triton_dist…
ptarasiewiczNV Feb 26, 2025
93250b9
update trd package name
ptarasiewiczNV Feb 26, 2025
f017e11
add cli
ptarasiewiczNV Feb 26, 2025
0b4f4c9
async output processing is working
ptarasiewiczNV Feb 26, 2025
d617364
linter
ptarasiewiczNV Feb 26, 2025
4505408
separate dockerfile for nixl for now
ptarasiewiczNV Feb 26, 2025
a2b8103
update build for vllm nixl
ptarasiewiczNV Feb 26, 2025
2f71a4e
nixl dockerfile
ptarasiewiczNV Feb 26, 2025
4ec974c
linter
ptarasiewiczNV Feb 26, 2025
f11a8b6
add note about nixl
ptarasiewiczNV Feb 26, 2025
757a570
update patch
Feb 26, 2025
e8f192a
Merge branch 'main' of github.com:triton-inference-server/triton_dist…
ptarasiewiczNV Feb 26, 2025
3bd66fe
incremental updates
nnshah1 Feb 27, 2025
1483276
Merge branch 'ptarasiewicz/vllm-nixl' into nnshah1-vllm-nixl-etcd
nnshah1 Feb 27, 2025
372ffca
adding copyright
nnshah1 Feb 27, 2025
3fb55a5
minor fixes for static checking
nnshah1 Feb 27, 2025
4752aa2
added to prevent errors for duplicate modules from mypy
nnshah1 Feb 27, 2025
30b898f
udpate todos
ptarasiewiczNV Feb 27, 2025
c83f153
Merge branch 'ptarasiewicz/vllm-nixl' of github.com:triton-inference-…
ptarasiewiczNV Feb 27, 2025
d25fe48
vllm patch 3710d93d172c4582acb0ffb48d7c5c14e86d6b2d
Feb 27, 2025
48094f0
move decode to be a generic worker with --remote-prefill flag
ptarasiewiczNV Feb 27, 2025
4f1be7e
add gap info
ptarasiewiczNV Feb 27, 2025
0ac6c5b
update patch to e5916c8134924e22a7824cc5cac1cabf49f9d78f
Feb 27, 2025
b2bff7d
Merge branch 'ptarasiewicz/vllm-nixl' of github.com:triton-inference-…
ptarasiewiczNV Feb 27, 2025
519ff81
add --remote-prefill cli flag
ptarasiewiczNV Feb 27, 2025
936c342
Merge branch 'ptarasiewicz/vllm-nixl' into nnshah1-vllm-nixl-etcd
nnshah1 Feb 27, 2025
9ca6b7d
working draft
nnshah1 Feb 27, 2025
3fe6e4e
Merge branch 'main' into nnshah1-vllm-nixl-etcd
Feb 27, 2025
293d298
incremental
Feb 27, 2025
483a176
incremental
nnshah1 Feb 28, 2025
a5ebcfe
Merge branch 'nnshah1-vllm-nixl-etcd' of https://github.com/triton-in…
nnshah1 Feb 28, 2025
663e818
Merge branch 'main' into nnshah1-vllm-nixl-etcd
Mar 1, 2025
e36c8b2
expose etcd kv create validate
ishandhanani Mar 1, 2025
c58cc07
updated core.pyi
ishandhanani Mar 2, 2025
c420cfd
updated
Mar 2, 2025
967b955
incremental
Mar 2, 2025
9abf497
updating standard workers - added support for etcd environment variable
Mar 2, 2025
10037da
Merge branch 'main' into nnshah1-vllm-nixl-etcd
Mar 2, 2025
eb9902a
updated to remove import
Mar 2, 2025
d66e0cc
updating pre-commit
Mar 2, 2025
d9e1b9b
Revert changes to dockerfile
Mar 2, 2025
936aec1
adding kv put and get prefix
ishandhanani Mar 2, 2025
4fba2c0
Merge remote-tracking branch 'origin/ishan/expose-etcd-bindings' into…
Mar 2, 2025
4ac8aa6
incremental
Mar 2, 2025
8b32db4
base working with rust etcd client
Mar 2, 2025
14be1b2
revert patch change
Mar 3, 2025
4a94788
cleanup
Mar 3, 2025
40e8a27
cleanup
Mar 3, 2025
4a991e2
Merge branch 'main' into nnshah1-vllm-nixl-etcd
ishandhanani Mar 3, 2025
56d6487
updated with pre commit
nnshah1 Mar 4, 2025
95f56f9
updating copyright
nnshah1 Mar 4, 2025
403e19b
updating
nnshah1 Mar 4, 2025
d80308c
updated
nnshah1 Mar 4, 2025
e1ea2e2
Merge branch 'main' into nnshah1-vllm-nixl-etcd
nnshah1 Mar 4, 2025
1988743
Merge branch 'main' into nnshah1-vllm-nixl-etcd
nnshah1 Mar 4, 2025
1687548
Merge branch 'nnshah1-vllm-nixl-etcd' of https://github.com/dynemo-ai…
nnshah1 Mar 4, 2025
17f3bda
Update container/run.sh
nnshah1 Mar 4, 2025
54e9ea8
revert adding
nnshah1 Mar 4, 2025
bf9fdb9
Merge branch 'nnshah1-vllm-nixl-etcd' of https://github.com/dynemo-ai…
nnshah1 Mar 4, 2025
ba926e8
cleanup
nnshah1 Mar 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions examples/python_rs/llm/vllm_nixl/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.utils import FlexibleArgumentParser

from triton_distributed.runtime import DistributedRuntime

METADATA_DIR = "/tmp/nixl"


Expand Down Expand Up @@ -63,3 +65,55 @@ def find_remote_metadata(engine_id):
msgspec.msgpack.decode(f.read(), type=NixlMetadata)
)
return remote_metadata


class NixlMetadataStore:
NIXL_METADATA_KEY = "nixl_metadata"

def __init__(self, namespace: str, runtime: DistributedRuntime) -> None:
self._namespace = namespace

# TODO Remove metadata from etcd on delete
self._stored: set[str] = set()

self._cached: dict[str, NixlMetadata] = {}
self._client = runtime.etcd_client()
self._key_prefix = f"{self._namespace}/{NixlMetadataStore.NIXL_METADATA_KEY}"

async def put(self, engine_id, metadata: NixlMetadata):
serialized_metadata = msgspec.msgpack.encode(metadata)
key = "/".join([self._key_prefix, engine_id])
await self._client.kv_put(key, serialized_metadata, None)
self._stored.add(engine_id)

async def get(self, engine_id) -> NixlMetadata:
try:
if engine_id in self._cached:
return self._cached[engine_id]

key = "/".join([self._key_prefix, engine_id])
key_values = await self._client.kv_get_prefix(key)
deserialized_metadata = None

for item in key_values:
deserialized_metadata = msgspec.msgpack.decode(
item["value"], type=NixlMetadata
)
break

if deserialized_metadata is None:
raise Exception("metadata not found in etcd")

self._cached[engine_id] = deserialized_metadata

# TODO watch for changes and update cache

# self._client.add_watch_callback(
# key,
# self._watch_callback,
# )

except Exception as e:
raise Exception("Error retrieving metadata for engine {engine_id}") from e

return deserialized_metadata
35 changes: 20 additions & 15 deletions examples/python_rs/llm/vllm_nixl/prefill_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@

import msgspec
import uvloop
from common import find_remote_metadata, parse_vllm_args
from vllm.distributed.device_communicators.nixl import NixlMetadata
from common import NixlMetadataStore, parse_vllm_args
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.entrypoints.openai.api_server import (
build_async_engine_client_from_engine_args,
Expand All @@ -31,8 +30,10 @@


class RequestHandler:
def __init__(self, engine_client):
def __init__(self, engine_client, metadata_store):
self.engine_client = engine_client
self._metadata_store = metadata_store
self._loaded_metadata = set()
print("RequestHandler initialized")

async def generate(self, raw_request: str):
Expand All @@ -50,6 +51,17 @@ async def generate(self, raw_request: str):
decode_engine_id=request.engine_id,
)

# TODO check if metadata has changed
# and reload - currently only loading once

if request.engine_id not in self._loaded_metadata:
remote_metadata = await self._metadata_store.get(request.engine_id)
await self.engine_client.add_remote_nixl_metadata(remote_metadata)
print(
f"Loaded nixl metadata from engine {request.engine_id} into engine {self.engine_client.nixl_metadata.engine_id}"
)
self._loaded_metadata.add(request.engine_id)

async for _ in self.engine_client.generate(
request_id=request.request_id,
prompt=TokensPrompt(prompt_token_ids=request.prompt_token_ids),
Expand All @@ -67,20 +79,13 @@ async def worker(runtime: DistributedRuntime, engine_args: AsyncEngineArgs):
endpoint = component.endpoint("generate")

async with build_async_engine_client_from_engine_args(engine_args) as engine_client:
# This should be replaced with etcd
metadata = engine_client.nixl_metadata
print(f"Waiting for remote metadata for engine {metadata.engine_id}")
remote_metadata: list[NixlMetadata] = []
while not remote_metadata:
await asyncio.sleep(1)
remote_metadata = find_remote_metadata(metadata.engine_id)

print(
f"Found {len(remote_metadata)} remote metadata for engine {metadata.engine_id}"
metadata_store = NixlMetadataStore("test-nixl", runtime)
await metadata_store.put(metadata.engine_id, metadata)

await endpoint.serve_endpoint(
RequestHandler(engine_client, metadata_store).generate
)
for remote_metadata in remote_metadata:
await engine_client.add_remote_nixl_metadata(remote_metadata)
await endpoint.serve_endpoint(RequestHandler(engine_client).generate)


if __name__ == "__main__":
Expand Down
22 changes: 12 additions & 10 deletions examples/python_rs/llm/vllm_nixl/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import msgspec
import uvloop
from common import parse_vllm_args, temp_metadata_file
from common import NixlMetadataStore, parse_vllm_args
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.engine.multiprocessing.client import EngineClient
from vllm.entrypoints.openai.api_server import (
Expand Down Expand Up @@ -132,15 +132,17 @@ async def worker(runtime: DistributedRuntime, engine_args: AsyncEngineArgs):

if engine_args.remote_prefill:
metadata = engine_client.nixl_metadata
with temp_metadata_file(metadata.engine_id, metadata):
await endpoint.serve_endpoint(
RequestHandler(
model_name="vllm",
engine_client=engine_client,
prefill_client=prefill_client,
do_remote_prefill=True,
).generate
)
metadata_store = NixlMetadataStore("test-nixl", runtime)
await metadata_store.put(metadata.engine_id, metadata)

await endpoint.serve_endpoint(
RequestHandler(
model_name="vllm",
engine_client=engine_client,
prefill_client=prefill_client,
do_remote_prefill=True,
).generate
)
else:
await endpoint.serve_endpoint(
RequestHandler(
Expand Down
83 changes: 82 additions & 1 deletion lib/bindings/python/rust/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
use futures::StreamExt;
use once_cell::sync::OnceCell;
use pyo3::exceptions::PyStopAsyncIteration;
use pyo3::types::PyString;
use pyo3::types::PyBytes;
use pyo3::types::{PyDict, PyList, PyString};
use pyo3::IntoPyObjectExt;
use pyo3::{exceptions::PyException, prelude::*};
use rs::pipeline::network::Ingress;
Expand Down Expand Up @@ -62,6 +63,7 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Component>()?;
m.add_class::<Endpoint>()?;
m.add_class::<Client>()?;
m.add_class::<EtcdClient>()?;
m.add_class::<AsyncResponseStream>()?;
m.add_class::<llm::kv::KvRouter>()?;
m.add_class::<llm::kv::KvMetricsPublisher>()?;
Expand All @@ -85,6 +87,12 @@ struct DistributedRuntime {
event_loop: PyObject,
}

#[pyclass]
#[derive(Clone)]
struct EtcdClient {
inner: rs::transports::etcd::Client,
}

#[pyclass]
#[derive(Clone)]
struct CancellationToken {
Expand Down Expand Up @@ -149,6 +157,12 @@ impl DistributedRuntime {
})
}

fn etcd_client(&self) -> PyResult<EtcdClient> {
Ok(EtcdClient {
inner: self.inner.etcd_client().clone(),
})
}

fn primary_token(&self) -> CancellationToken {
let inner = self.inner.runtime().primary_token();
CancellationToken { inner }
Expand Down Expand Up @@ -252,6 +266,73 @@ impl Namespace {
}
}

#[pymethods]
impl EtcdClient {
#[pyo3(signature = (key, value, lease_id=None))]
fn kv_create_or_validate<'p>(
Comment thread
nnshah1 marked this conversation as resolved.
&self,
py: Python<'p>,
key: String,
value: Vec<u8>,
lease_id: Option<i64>,
) -> PyResult<Bound<'p, PyAny>> {
let client = self.inner.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
client
.kv_create_or_validate(key, value, lease_id)
.await
.map_err(to_pyerr)?;
Ok(())
})
}

#[pyo3(signature = (key, value, lease_id=None))]
fn kv_put<'p>(
&self,
py: Python<'p>,
key: String,
value: Vec<u8>,
lease_id: Option<i64>,
) -> PyResult<Bound<'p, PyAny>> {
let client = self.inner.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
client
.kv_put(key, value, lease_id)
.await
.map_err(to_pyerr)?;
Ok(())
})
}

fn kv_get_prefix<'p>(&self, py: Python<'p>, prefix: String) -> PyResult<Bound<'p, PyAny>> {
let client = self.inner.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let result = client
.kv_get_prefix(prefix)
.await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;

// Convert Vec<KeyValue> to a list of dictionaries
let py_list = Python::with_gil(|py| {
let list = PyList::empty(py);
for kv in result {
let dict = PyDict::new(py);
dict.set_item("key", String::from_utf8_lossy(kv.key()).to_string())?;
dict.set_item("value", PyBytes::new(py, kv.value()))?;
Comment thread
nnshah1 marked this conversation as resolved.
dict.set_item("create_revision", kv.create_revision())?;
dict.set_item("mod_revision", kv.mod_revision())?;
dict.set_item("version", kv.version())?;
dict.set_item("lease", kv.lease())?;
list.append(dict)?;
}
Ok::<Py<PyList>, PyErr>(list.into())
})?;

Ok(py_list)
})
}
}

#[pymethods]
impl Client {
/// Get list of current endpoints
Expand Down
30 changes: 29 additions & 1 deletion lib/bindings/python/src/triton_distributed/_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import AsyncGenerator, AsyncIterator, Callable, List
from typing import AsyncGenerator, AsyncIterator, Callable, Dict, List, Optional

class JsonLike:
"""
Expand All @@ -37,6 +37,34 @@ class DistributedRuntime:
"""
...

def etcd_client(self) -> EtcdClient:
"""
Get the `EtcdClient` object
"""
...

class EtcdClient:
"""
Etcd is used for discovery in the DistributedRuntime
"""
async def kv_create_or_validate(self, key: str, value: bytes, lease_id: Optional[int] = None) -> None:
"""
Atomically create a key if it does not exist, or validate the values are identical if the key exists.
"""
...

async def kv_put(self, key: str, value: bytes, lease_id: Optional[int] = None) -> None:
"""
Put a key-value pair into etcd
"""
...

async def kv_get_prefix(self, prefix: str) -> List[Dict[str, JsonLike]]:
"""
Get all keys with a given prefix
"""
...

class Namespace:
"""
A namespace is a collection of components
Expand Down
68 changes: 68 additions & 0 deletions lib/bindings/python/tests/test_etcd_bindings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio

from triton_distributed._core import DistributedRuntime


async def test_simple_put_get():
# Initialize runtime
loop = asyncio.get_running_loop()
runtime = DistributedRuntime(loop)

# Get etcd client
etcd = runtime.etcd_client()

# Write some key-value pairs
test_keys = {
"test/key1": b"value1",
"test/key2": b"value2",
"test/nested/key3": b"value3",
}
# Write each key-value pair
for key, value in test_keys.items():
print(f"Writing {key} = {value!r}")
await etcd.kv_create_or_validate(key, value, None)

print("Successfully wrote all keys to etcd")

# Test kv_put
put_key = "test/put_key"
put_value = b"put_value"
test_keys[put_key] = put_value
print(f"Using kv_put to write {put_key} = {put_value!r}")
await etcd.kv_put(put_key, put_value, None)

# Test kv_get_prefix to read all keys
print("\nReading all keys with prefix 'test/':")
keys_values = await etcd.kv_get_prefix("test/")
for item in keys_values:
print(f"Retrieved {item['key']} = {item['value']!r}")
assert test_keys[item["key"]] == item["value"]

# Verify prefix filtering works
print("\nReading keys with prefix 'test/nested/':")
nested_keys_values = await etcd.kv_get_prefix("test/nested/")
for item in nested_keys_values:
print(f"Retrieved {item['key']} = {item['value']!r}")
assert test_keys[item["key"]] == item["value"]

# Shutdown runtime
runtime.shutdown()


if __name__ == "__main__":
asyncio.run(test_simple_put_get())