Skip to content
Merged
Show file tree
Hide file tree
Changes from 72 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
16609c2
copied in unified_memory.py.
ArEsKay3 Sep 9, 2025
f32fdf2
connected arg to context.
lmcafee-nvidia Sep 9, 2025
beb7f7d
minor fixes.
lmcafee-nvidia Sep 9, 2025
c641430
added unit test.
lmcafee-nvidia Sep 9, 2025
2d05368
clean up.
lmcafee-nvidia Sep 9, 2025
088b320
chore: Format files
Sep 9, 2025
f322734
fix formatting and imports for ci.
lmcafee-nvidia Sep 9, 2025
330c555
chore: Format files
Sep 9, 2025
4e8148f
Clean up unified memory code
tdene Sep 9, 2025
55a528e
Fix pipeline
tdene Sep 9, 2025
ca2cdd3
lint
tdene Sep 9, 2025
074e908
updated prints/warnings.
lmcafee-nvidia Sep 9, 2025
d1e9c4d
Pytest skip if unified memory is not available
tdene Sep 9, 2025
8187df3
chore: Format files
Sep 9, 2025
18c589e
Address reviewer comments
tdene Sep 9, 2025
1136dbd
Merge remote-tracking branch 'lm/lmcafee/dyn-inf-unified-memory' into…
tdene Sep 9, 2025
827f055
Merge remote-tracking branch 'origin/main' into lmcafee/dyn-inf-unifi…
lmcafee-nvidia Sep 16, 2025
618b3cf
tested different compilation setups.
lmcafee-nvidia Sep 22, 2025
ea48697
clean up.
lmcafee-nvidia Sep 22, 2025
7561a7f
Merge remote-tracking branch 'origin/main' into lmcafee/dyn-inf-unifi…
lmcafee-nvidia Sep 22, 2025
2eed5a8
chore: Format files
Sep 22, 2025
b14c607
Import a mempool-builder instead of global mempool
tdene Sep 22, 2025
df8ca83
Fix import issue
tdene Sep 22, 2025
ff43bf4
Clean-up, get ready for merge
tdene Sep 22, 2025
6922c76
Fix tests
tdene Sep 22, 2025
c05eb2a
sep 30 alpha.
lmcafee-nvidia Sep 30, 2025
eba0a47
oct 1 alpha.
lmcafee-nvidia Oct 1, 2025
f604162
fixed max_requests.
lmcafee-nvidia Oct 1, 2025
62ce81c
fixed newly_paused_request_ids.
lmcafee-nvidia Oct 2, 2025
1a74d5c
fixed resume_request_count; mostly vectorized.
lmcafee-nvidia Oct 2, 2025
9e0b2b3
fixed paused_chunk_counts.
lmcafee-nvidia Oct 2, 2025
db6dbdd
max_requests -> max_{total,active}_requests.
lmcafee-nvidia Oct 6, 2025
db36ad0
available memory determined by available active chunks.
lmcafee-nvidia Oct 6, 2025
96c2656
added methods to compute active/paused used/avail chunk counts.
lmcafee-nvidia Oct 6, 2025
42c0777
minor clean up.
lmcafee-nvidia Oct 6, 2025
ee9a09a
no adding requests if paused requests.
lmcafee-nvidia Oct 6, 2025
f520304
passed context to chunk allocator's constructor.
lmcafee-nvidia Oct 7, 2025
12b9866
some clean up.
lmcafee-nvidia Oct 7, 2025
185ffbf
4gb ran smoothly.
lmcafee-nvidia Oct 7, 2025
f45f3bf
fixed synthetic prompts to have expected length.
lmcafee-nvidia Oct 9, 2025
eb7bb9d
fixed resume_request_count.
lmcafee-nvidia Oct 13, 2025
7a0934f
clean up.
lmcafee-nvidia Oct 16, 2025
f93e4de
minor updates.
lmcafee-nvidia Oct 16, 2025
40f08b7
Merge remote-tracking branch 'origin/main' into lmcafee/dyn-context-u…
lmcafee-nvidia Oct 16, 2025
b4d32ef
chore: Format files
Oct 16, 2025
55b14a0
fixed max_requests rename.
lmcafee-nvidia Oct 16, 2025
2c0d225
Merge remote-tracking branch 'origin/lmcafee/dyn-context-uvm-only' in…
lmcafee-nvidia Oct 16, 2025
3bdd661
removed placeholder methods for computing buffer_size and max_tokens.
lmcafee-nvidia Oct 29, 2025
559daf3
re-added unified_memory_level control to dynamic context.
lmcafee-nvidia Oct 30, 2025
be51f16
clean up.
lmcafee-nvidia Oct 30, 2025
991c95a
added uvm level to print.
lmcafee-nvidia Oct 30, 2025
88e412f
format.
lmcafee-nvidia Oct 30, 2025
5e61868
Merge remote-tracking branch 'origin/main' into lmcafee/dyn-context-u…
lmcafee-nvidia Oct 30, 2025
d7c5eae
using correct sampling_params during build_requests().
lmcafee-nvidia Oct 30, 2025
bf1a342
running again.
lmcafee-nvidia Oct 30, 2025
73b7041
minor update.
lmcafee-nvidia Oct 30, 2025
191fe45
Merge remote-tracking branch 'origin/main' into lmcafee/dyn-context-u…
lmcafee-nvidia Oct 30, 2025
f2548c6
fixed test_dynamic_context.py.
lmcafee-nvidia Oct 31, 2025
fa23ce8
default uvm level == 1; assert(max_tokens >= max_active_requests).
lmcafee-nvidia Oct 31, 2025
c83b373
formalized DEFAULT_MAX_TOKENS.
lmcafee-nvidia Oct 31, 2025
0c387f5
test simple runs with and without triton kernel.
lmcafee-nvidia Oct 31, 2025
717dfe9
test_dynamic_engine.py works.
lmcafee-nvidia Oct 31, 2025
152de82
Merge remote-tracking branch 'main/main' into lmcafee/dyn-context-uvm…
lmcafee-nvidia Oct 31, 2025
e4c6554
format.
lmcafee-nvidia Oct 31, 2025
ed8e9b3
Merge remote-tracking branch 'main/main' into lmcafee/dyn-context-uvm…
lmcafee-nvidia Nov 3, 2025
399379b
Merge remote-tracking branch 'main/main' into lmcafee/dyn-context-uvm…
lmcafee-nvidia Nov 3, 2025
769c1db
fixed test_dynamic_engine.py.
lmcafee-nvidia Nov 4, 2025
c957a1d
fixed test_simple_text_generation_controller.py.
lmcafee-nvidia Nov 4, 2025
da886a0
fixed test_dynamic_context.py.
lmcafee-nvidia Nov 4, 2025
a54cec9
Merge remote-tracking branch 'main/main' into lmcafee/dyn-context-uvm…
lmcafee-nvidia Nov 4, 2025
4593de3
format.
lmcafee-nvidia Nov 4, 2025
20f1981
Merge remote-tracking branch 'main/main' into lmcafee/dyn-context-uvm…
lmcafee-nvidia Nov 7, 2025
9b6125d
minor fix.
lmcafee-nvidia Nov 7, 2025
feef5d5
fixed dynamic context instantiation from static engine.
lmcafee-nvidia Nov 7, 2025
96c1f00
Merge remote-tracking branch 'main/main' into lmcafee/dyn-context-uvm…
lmcafee-nvidia Nov 7, 2025
ee078e5
fixed wandb tests.
lmcafee-nvidia Nov 7, 2025
5e6f38d
Merge remote-tracking branch 'main/main' into lmcafee/dyn-context-uvm…
lmcafee-nvidia Nov 7, 2025
cec714e
Merge remote-tracking branch 'main/main' into lmcafee/dyn-context-uvm…
lmcafee-nvidia Nov 10, 2025
7cd2edd
integrated mamba updates.
lmcafee-nvidia Nov 10, 2025
4db13d4
re-added max_tokens arg.
lmcafee-nvidia Nov 10, 2025
872acba
fixed test_dynamic_context.py.
lmcafee-nvidia Nov 10, 2025
3ee18fe
second usage of 'with ctx_manager:' fails when using uvm.
lmcafee-nvidia Nov 10, 2025
e5a3276
using single 'with ctx_manager:' block.
lmcafee-nvidia Nov 10, 2025
d493a98
Merge remote-tracking branch 'main/main' into lmcafee/dyn-context-uvm…
lmcafee-nvidia Nov 10, 2025
f4f60a9
format.
lmcafee-nvidia Nov 10, 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
23 changes: 10 additions & 13 deletions examples/inference/gpt/gpt_dynamic_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def get_inference_context(
"""The inference context manages the KV cache and other inference state."""

args = get_args()

# Max sequence length.
if calculate_max_sequence_length_from_requests:
max_gen_length = sampling_params.num_tokens_to_generate
Expand Down Expand Up @@ -147,11 +148,7 @@ def get_inference_context(
else None
),
block_size_tokens=args.inference_dynamic_batching_block_size,
buffer_size_gb=args.inference_dynamic_batching_buffer_size_gb,
buffer_guaranteed_fraction=args.inference_dynamic_batching_buffer_guaranteed_fraction,
buffer_overflow_factor=args.inference_dynamic_batching_buffer_overflow_factor,
max_requests_override=args.inference_dynamic_batching_max_requests_override,
max_tokens_override=args.inference_dynamic_batching_max_tokens_override,
active_buffer_size_gb=args.inference_dynamic_batching_active_buffer_size_gb,
tensor_model_parallel_size=args.tensor_model_parallel_size,
materialize_only_last_token_logits=not args.return_log_probs,
cache_mla_latent=args.multi_latent_attention and args.cache_mla_latents,
Expand Down Expand Up @@ -514,14 +511,14 @@ def escape_str(s):
if engine.capture_stats else
"--"
)
print(
f"{setup_prefix} … "
f"capture {capture_str} … "
f"mem {peak_alloc_gb:.1f}/{peak_resvd_gb:.1f} GB … "
f"total time: {total_time:.3f}s … "
f"steps: {engine.step_count:d} … "
f"throughput: {throughput:.3f} tok/s"
)
print(" … ".join((
f"{setup_prefix}",
f"throughput: {throughput:.3f} tok/s",
f"total time: {total_time:.3f}s",
f"mem {peak_alloc_gb:.1f}/{peak_resvd_gb:.1f} GB",
f"steps: {engine.step_count:d}",
f"capture {capture_str}",
)))
print("~~~")

# Stop Nsight profiler.
Expand Down
8 changes: 2 additions & 6 deletions examples/inference/gpt/gpt_dynamic_inference_12b.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ export CUDA_DEVICE_MAX_CONNECTIONS=1
: ${INCOMING_REQUESTS_PER_SEC=100.}

# Dynamic context.
: ${BUFFER_SIZE_GB=50.}
: ${BUFFER_OVERFLOW_FACTOR=1.}
: ${BUFFER_GUARANTEED_FRACTION=0.05}
: ${ACTIVE_BUFFER_SIZE_GB=50.}

# Cuda graphs.
: ${CUDA_GRAPH_IMPL=local}
Expand Down Expand Up @@ -78,9 +76,7 @@ ARGS=" \
--inference-rng-tracker \
\
--inference-dynamic-batching \
--inference-dynamic-batching-buffer-size-gb ${BUFFER_SIZE_GB} \
--inference-dynamic-batching-buffer-overflow-factor ${BUFFER_OVERFLOW_FACTOR} \
--inference-dynamic-batching-buffer-guaranteed-fraction ${BUFFER_GUARANTEED_FRACTION} \
--inference-dynamic-batching-active-buffer-size-gb ${ACTIVE_BUFFER_SIZE_GB} \
\
${EXTRA_ARGS} \
"
Expand Down
8 changes: 2 additions & 6 deletions examples/inference/gpt/gpt_dynamic_inference_357m.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ export CUDA_DEVICE_MAX_CONNECTIONS=1
: ${INCOMING_REQUESTS_PER_SEC=100.}

# Dynamic context.
: ${BUFFER_SIZE_GB=50.}
: ${BUFFER_OVERFLOW_FACTOR=1.}
: ${BUFFER_GUARANTEED_FRACTION=0.05}
: ${ACTIVE_BUFFER_SIZE_GB=50.}

# Cuda graphs.
: ${CUDA_GRAPH_IMPL=local}
Expand Down Expand Up @@ -64,9 +62,7 @@ ARGS=" \
--inference-rng-tracker \
\
--inference-dynamic-batching \
--inference-dynamic-batching-buffer-size-gb ${BUFFER_SIZE_GB} \
--inference-dynamic-batching-buffer-overflow-factor ${BUFFER_OVERFLOW_FACTOR} \
--inference-dynamic-batching-buffer-guaranteed-fraction ${BUFFER_GUARANTEED_FRACTION} \
--inference-dynamic-batching-active-buffer-size-gb ${ACTIVE_BUFFER_SIZE_GB} \
\
${EXTRA_ARGS} \
"
Expand Down
40 changes: 25 additions & 15 deletions examples/inference/gpt/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from megatron.core.inference.inference_request import DynamicInferenceRequest
from megatron.core.inference.contexts import DynamicInferenceContext
from megatron.core.inference.contexts.dynamic_context import get_mem_size_str
from megatron.core.transformer.module import MegatronModule

from megatron.core.inference.sampling_params import SamplingParams
Expand Down Expand Up @@ -65,7 +66,7 @@ def add_common_inference_args(parser: ArgumentParser) -> ArgumentParser:
help="Add a deterministic number of requests per step. This arg is "
"prioritized over `--incoming-requests-per-sec` below (which is non-"
"deterministic). Note that the number of requests added per step is "
"additionally limited by the inference context's `max_requests`, "
"additionally limited by the inference context's `max_active_requests`, "
"`max_tokens`, and KV buffer size.",
)
group.add_argument(
Expand Down Expand Up @@ -259,10 +260,27 @@ def get_synthetic_requests(
int(args.incoming_requests_per_sec * args.incoming_requests_duration),
)

# Build prompts with expected lengths.
assert (
len(args.num_tokens_to_prompt) == 2
and
args.num_tokens_to_prompt[1] >= args.num_tokens_to_prompt[0]
)
max_prompt_length = args.num_tokens_to_prompt[1]
max_prompt_text = "hi " * max_prompt_length
max_prompt_tokens = tokenizer.tokenize(max_prompt_text)
prompt_lengths = [
random.randint(*args.num_tokens_to_prompt)
for _ in time_offsets
]
prompt_tokens_list = [ max_prompt_tokens[:l] for l in prompt_lengths ]
prompt_texts = [ tokenizer.detokenize(tt) for tt in prompt_tokens_list ]

# Init requests.
assert len(prompt_texts) == len(time_offsets)
requests = [
Request("hi " * random.randint(*args.num_tokens_to_prompt), t, tokenizer, sampling_params)
for t in time_offsets
Request(t, o, tokenizer, sampling_params=sampling_params)
for t, o in zip(prompt_texts, time_offsets)
]

return requests
Expand Down Expand Up @@ -339,7 +357,7 @@ def build_dynamic_engine_setup_prefix(

Args:
args (Namespace): Command-line arguments for this run.
context (DynamicInferenceContext): Stores limits such as `max_requests`,
context (DynamicInferenceContext): Stores limits such as `max_active_requests`,
`max_tokens`, and `gtd_request_count`.
requests (List[DynamicInferenceRequest]): List of inference requests.

Expand Down Expand Up @@ -376,17 +394,10 @@ def build_dynamic_engine_setup_prefix(
)

# Buffer limits config
flw = args.inference_dynamic_batching_buffer_overflow_factor
flw_str = "no overflow" if flw is None else f"{flw:.1f}"
buffer_limits_str = (
f"bf {args.inference_dynamic_batching_buffer_size_gb:.0f}, {flw_str} "
f"[r {context.max_requests}, t {context.max_tokens}]"
)

# Guaranteed request config
guaranteed_fraction_str = (
f"gtd {args.inference_dynamic_batching_buffer_guaranteed_fraction:.2f} "
f"[r {context.gtd_request_count}]"
f"bf: {get_mem_size_str(args.inference_dynamic_batching_active_buffer_size_gb*1024**3)}, "
f"{context.block_allocator.active_count} chunks "
f"[r {context.max_active_requests}, t {context.max_tokens}]"
)

parts = [
Expand All @@ -396,7 +407,6 @@ def build_dynamic_engine_setup_prefix(
uvm_str,
request_str,
buffer_limits_str,
guaranteed_fraction_str,
]

return " | ".join(parts)
86 changes: 56 additions & 30 deletions megatron/core/inference/contexts/dynamic_block_allocator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,60 +13,86 @@ class BlockAllocator:
- Initializing a pool of block IDs
- Allocating blocks from the pool
- Releasing blocks back to the pool
- Managing the guaranteed block count for active requests

Args:
block_count_total (int): Total number of blocks available in the buffer.
gtd_block_count (int): Number of blocks reserved for guaranteed requests.
context (DynamicInferenceContext): Dynamic inference context.
active_count (int): Total number of active blocks available in the buffer.
The full buffer size is 2*active_count, to accommodate an equal-size
space for paused requests that live on the CPU.
"""

def __init__(self, block_count_total: int, gtd_block_count: int):
self.block_count_total = block_count_total
self.gtd_block_count = gtd_block_count
def __init__(self, context: "DynamicInferenceContext", active_count: int):

# Reserve last block ID as dummy block for decode-only inference steps
self.block_count_avail = self.block_count_total - 1
self.dummy_block_idx = self.block_count_total - 1
self.context = context

active_count -= 1 # -1 for dummy_block_idx (see below)
active_count = max(1, active_count) # need at least one block
self.total_count = 2 * active_count + 1 # +1 for dummy_block_idx
self.total_avail = self.total_count - 1 # -1 for dummy_block_idx
self.active_count = active_count
self.paused_count = self.total_count - self.active_count - 1 # -1 for dummy_block_idx
self.dummy_block_idx = self.total_count - 1

# Initialize block pool as a "stack" data structure
self.block_bag = torch.arange(
self.block_count_total, dtype=torch.int32, device=torch.cuda.current_device()
self.total_count, dtype=torch.int32, device=torch.cuda.current_device()
)

def is_memory_available(self, num_blocks: int, safe: bool = False) -> bool:
"""Check if memory blocks are available.
def __str__(self):
return (
f"total avail {self.total_avail} / {self.total_count - 1}"
f"; active {self.active_count}"
)

Use 'safe' to avoid all requests being deadlocked. A fraction of the KV cache
memory buffer is reserved to guarantee that a minimum number of active
requests can run on any given step.
def get_active_used(self):
"""Compute number of active blocks used."""
return (
self.context.request_kv_block_counts[
self.context.paused_request_count : self.context.total_request_count
]
.sum()
.item()
)

def get_paused_used(self):
"""Compute number of paused blocks used."""
return (
self.context.request_kv_block_counts[: self.context.paused_request_count].sum().item()
)

def get_active_avail(self):
"""Compute number of active blocks available."""
return self.active_count - self.get_active_used()

def get_paused_avail(self):
"""Compute number of paused blocks available."""
return self.paused_count - self.get_paused_used()

def is_memory_available(self, num_blocks: int) -> bool:
"""Check if memory blocks are available.

Args:
num_blocks (int): Number of blocks to check.
safe (bool): Include extra space for guaranteeing ability to run
requests to completion.

Return:
(bool) Is memory available?
"""
if safe:
return self.block_count_avail >= num_blocks + self.gtd_block_count
else:
return self.block_count_avail >= num_blocks
return self.get_active_avail() >= num_blocks

def allocate_memory_blocks(self, num_blocks: int = 1, safe: bool = False) -> Optional[Tensor]:
def allocate_memory_blocks(self, num_blocks: int) -> Optional[Tensor]:
"""Allocate memory blocks if available, else return None.

Args:
num_blocks (int): Number of blocks to allocate.
safe (bool): Include extra space for guaranteeing ability to run
requests to completion.

Return:
(Optional[Tensor]) Allocated block IDs.
"""
if self.is_memory_available(num_blocks, safe):
self.block_count_avail -= num_blocks
return self.block_bag[self.block_count_avail : (self.block_count_avail + num_blocks)]
if self.is_memory_available(num_blocks):
self.total_avail -= num_blocks
block_ids = self.block_bag[self.total_avail : (self.total_avail + num_blocks)]
assert num_blocks == block_ids.numel()
return block_ids
else:
return None

Expand All @@ -80,13 +106,13 @@ def release_memory_blocks(self, blocks: Tensor) -> None:
None
"""
num_blocks = blocks.size(dim=0)
self.block_bag[self.block_count_avail : (self.block_count_avail + num_blocks)] = blocks
self.block_count_avail += num_blocks
self.block_bag[self.total_avail : (self.total_avail + num_blocks)] = blocks
self.total_avail += num_blocks

def reset(self) -> None:
"""Reset the allocator to initial state.

This resets the available block count to the entire memory pool
(except for the dummy block).
"""
self.block_count_avail = self.block_count_total - 1
self.total_avail = self.total_count - 1
Loading