Instructions to use KexuanShi/Megatron-LM with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- NeMo
How to use KexuanShi/Megatron-LM with NeMo:
# tag did not correspond to a valid NeMo domain.
- Notebooks
- Google Colab
- Kaggle
| # Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| import copy | |
| import json | |
| import itertools | |
| import random | |
| import time | |
| import torch | |
| from argparse import ArgumentParser, Namespace | |
| from tqdm import tqdm | |
| from typing import Any, List, Optional | |
| 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 | |
| def add_common_inference_args(parser: ArgumentParser) -> ArgumentParser: | |
| """Common inference arguments.""" | |
| group = parser.add_argument_group(title='Common inference') | |
| group.add_argument("--temperature", type=float, default=1.0, help='Sampling temperature.') | |
| group.add_argument("--top_k", type=int, default=1, help='Top k sampling.') | |
| group.add_argument("--top_p", type=float, default=0.0, help='Top p sampling.') | |
| group.add_argument( | |
| "--return-log-probs", | |
| action='store_true', | |
| default=False, | |
| help='Return the log probabilities of the final output tokens', | |
| ) | |
| group.add_argument( | |
| "--prompts", | |
| metavar='N', | |
| type=str, | |
| nargs='+', | |
| help='Input prompts with each prompt within quotes and seperated by space', | |
| ) | |
| group.add_argument( | |
| "--num-tokens-to-prompt", | |
| type=int, | |
| nargs="+", | |
| default=[64, 1024], | |
| help='Number of tokens to use for simulated prompts. This should be a ' | |
| 'space-separated pair of integers, and the generated prompt lengths will ' | |
| 'be uniformly sampled within this range.', | |
| ) | |
| group.add_argument( | |
| "--num-tokens-to-generate", | |
| type=int, | |
| default=30, | |
| help='Number of tokens to generate for each prompt', | |
| ) | |
| group.add_argument( | |
| "--num-tokens-from-file", | |
| action='store_true', | |
| default=False, | |
| help='Use per-prompt num_tokens_to_generate from prompt file', | |
| ) | |
| group.add_argument( | |
| "--top-n-logprobs", | |
| type=int, | |
| default=0, | |
| help='Return the top n logprobs for the generated tokens and their corresponding token as a dictionary', | |
| ) | |
| group.add_argument( | |
| "--incoming-requests-per-step", | |
| type=int, default=None, | |
| 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`, " | |
| "`max_tokens`, and KV buffer size.", | |
| ) | |
| group.add_argument( | |
| "--incoming-requests-per-sec", | |
| type=float, | |
| default=100.0, | |
| help="Simulated number of requests per second. Set to -1 to add all requests together.", | |
| ) | |
| group.add_argument( | |
| "--incoming-requests-duration", | |
| type=float, | |
| default=10.0, | |
| help="Total amount of time to simulate that requests are " | |
| "arriving. Multiply this value with " | |
| "`--incoming-requests-per-sec` to get the approximate " | |
| "total number of requests. Set to -1 to add all requests together.", | |
| ) | |
| group.add_argument( | |
| "--model-provider", | |
| choices=["mamba", "gpt"], | |
| default="gpt", | |
| help="Model provider", | |
| ) | |
| group.add_argument( | |
| "--skip-prompt-log-probs", | |
| action='store_true', | |
| default=False, | |
| help='Skip prompt log probs.', | |
| ) | |
| group.add_argument( | |
| "--stop-words", | |
| metavar='WORD', | |
| type=str, | |
| nargs='+', | |
| default=None, | |
| help='Stop words to terminate generation. Each word should be quoted and ' | |
| 'separated by space. Example: --stop-words "\\n\\n" "END" "###"', | |
| ) | |
| group.add_argument( | |
| "--output-path", | |
| type=str, | |
| default=None, | |
| help="Path to save generations as JSON", | |
| ) | |
| group.add_argument( | |
| "--output-every-n-results", | |
| type=int, | |
| default=1, | |
| help="To minimize the output file size of larger runs, only write the " | |
| "results of every `n` requests.", | |
| ) | |
| group.add_argument( | |
| "--prompt-file", | |
| help='Jsonl file containing input prompts, where each item (i.e., line) ' | |
| 'contains the field \'text\' where the value is the prompt. All other ' | |
| 'fields within each item are ignored, and may be customized for each ' | |
| 'application.', | |
| ) | |
| group.add_argument( | |
| "--prompt-file-num-truncate", | |
| type=int, | |
| help='Number of samples to use from the loaded prompt file (see ' | |
| '`--prompt-file` above). The first `--prompt-file-num-truncate` samples ' | |
| 'will be used, in order.', | |
| ) | |
| group.add_argument( | |
| "--use-flashinfer-fused-rope", | |
| action='store_true', | |
| default=False, | |
| help='Use flashinfer fused rope implementation.', | |
| ) | |
| group.add_argument( | |
| "--no-record-throughput", | |
| action='store_false', | |
| dest="record_throughput", | |
| help="Disable throughput recording in --output-file" | |
| ) | |
| return parser | |
| def get_default_sampling_params(termination_id: int = None): | |
| return SamplingParams( | |
| temperature=1.0, | |
| top_k=1, | |
| top_p=0.0, | |
| return_log_probs=False, | |
| num_tokens_to_generate=30, | |
| termination_id = termination_id, | |
| ) | |
| def get_curr_time() -> float: | |
| """Get synchronized time across ranks.""" | |
| curr_time = torch.cuda.LongTensor([time.time_ns()]) | |
| if torch.distributed.is_initialized(): | |
| torch.distributed.broadcast(curr_time, src=0) | |
| return curr_time.item() / 10**9 | |
| class Request: | |
| """Class to hold attributes for a single request. | |
| A request is initialized with its prompt text. As it is added, processed, | |
| and completed through the inference engine, the request is populated with its | |
| start time, end time, and output tokens. | |
| Args: | |
| prompt_text (str): Prompt text. | |
| time_offset (float): Artificial time offset for simulating incoming | |
| requests. This value is later added to the `base_arrival_time` to | |
| simulate the requests arrival time. | |
| tokenizer (Any): Tokenizer for tokenizing the prompt. | |
| """ | |
| def __init__(self, prompt_text: str, time_offset: float, tokenizer: Any, sampling_params: SamplingParams = None): | |
| self.prompt_text = prompt_text | |
| self.prompt_tokens = tokenizer.tokenize(prompt_text) | |
| self.output_text = None | |
| self.output_tokens = [] | |
| self.time_offset = time_offset | |
| self.time_arrival = None | |
| self.time_start = None | |
| self.time_end = None | |
| self.state = "not-started" | |
| self.sampling_params: SamplingParams = sampling_params if sampling_params is not None else get_default_sampling_params(tokenizer.eod) | |
| self.sampling_params = copy.deepcopy(self.sampling_params) | |
| def __str__(self) -> str: | |
| return "state '%s'; toffset %.1e; prompt len %d; output len %d; '%s'" % ( | |
| self.state, | |
| self.time_offset, | |
| len(self.prompt_tokens), | |
| len(self.output_tokens), | |
| self.prompt_text, | |
| ) | |
| def get_time_offsets( | |
| seed: int | None, | |
| incoming_requests_per_step: int, | |
| incoming_requests_per_sec: float, | |
| num_requests: int, | |
| ) -> list[float]: | |
| """Get example time offsets.""" | |
| # Time offsets to add all requests at once. | |
| if incoming_requests_per_step is not None or incoming_requests_per_sec <= 0: | |
| return [-1] * num_requests | |
| # if num_requests is not None: | |
| incoming_requests_duration = num_requests / incoming_requests_per_sec | |
| incoming_requests_duration *= 2 # extra margin, to accomodate time sampling | |
| random.seed(seed) | |
| import simpy # Guard against this import in test case | |
| # Generate random time offsets. | |
| def arrival(r): | |
| while True: | |
| yield env.timeout(random.expovariate(r)) | |
| time_offsets.append(env.now) | |
| time_offsets = [] | |
| env = simpy.Environment() | |
| env.process(arrival(incoming_requests_per_sec)) | |
| env.run(incoming_requests_duration) | |
| # Ensure at least a single request. | |
| if len(time_offsets) == 0: | |
| time_offsets = [0.0] | |
| # Ensure first time is 0. | |
| time_offsets = [to - time_offsets[0] for to in time_offsets] | |
| # Truncate to num_requests. | |
| assert len(time_offsets) >= num_requests | |
| time_offsets = time_offsets[:num_requests] | |
| return time_offsets | |
| def get_cli_requests( | |
| args: Namespace, tokenizer: Any, sampling_params: Optional[SamplingParams] = None | |
| ) -> list[Request]: | |
| # Get time offsets. | |
| t_offsets = get_time_offsets( | |
| args.seed, | |
| args.incoming_requests_per_step, | |
| args.incoming_requests_per_sec, | |
| len(args.prompts), | |
| ) | |
| # Init requests. | |
| requests = [Request(p, t, tokenizer, sampling_params) for p,t in zip(args.prompts, t_offsets)] | |
| return requests | |
| def get_synthetic_requests( | |
| args: Namespace, tokenizer: Any, sampling_params: Optional[SamplingParams] = None | |
| ) -> list[Request]: | |
| """Get example requests.""" | |
| # Get time offsets. | |
| time_offsets = get_time_offsets( | |
| args.seed, | |
| args.incoming_requests_per_step, | |
| args.incoming_requests_per_sec, | |
| 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(t, o, tokenizer, sampling_params=sampling_params) | |
| for t, o in zip(prompt_texts, time_offsets) | |
| ] | |
| return requests | |
| def get_requests_from_file( | |
| args: Namespace, tokenizer: Any, sampling_params: Optional[SamplingParams] = None | |
| ) -> list[Request]: | |
| """Get requests from a file.""" | |
| if not args.prompt_file: | |
| raise ValueError("Prompt file is required to read requests from a file.") | |
| # Load prompts. | |
| n_prompts = sum(1 for _ in open(args.prompt_file)) | |
| prompts = [] | |
| if sampling_params is None: | |
| sampling_params = get_default_sampling_params(tokenizer.eod) | |
| sampling_params_list = [] | |
| with open(args.prompt_file) as f: | |
| for line in tqdm(f.readlines(), "read prompt file", total=n_prompts): | |
| line_dict = json.loads(line) | |
| prompts.append(line_dict["text"]) | |
| sp = copy.deepcopy(sampling_params) | |
| if args.num_tokens_from_file: | |
| sp.num_tokens_to_generate = line_dict["chatgpt_output_token_length"] | |
| sampling_params_list.append(sp) | |
| if len(prompts) == args.prompt_file_num_truncate: | |
| break | |
| # Get time offsets. | |
| time_offsets: list[float] = get_time_offsets( | |
| args.seed, | |
| args.incoming_requests_per_step, | |
| args.incoming_requests_per_sec, | |
| len(prompts), | |
| ) | |
| # Init requests. | |
| requests = [ | |
| Request(p, t, tokenizer, sp) | |
| for p, t, sp in tqdm(zip(prompts, time_offsets, sampling_params_list), "init requests", total=len(prompts)) | |
| ] | |
| return requests | |
| def build_requests( | |
| args: Namespace, tokenizer: Any, sampling_params: Optional[SamplingParams] = None | |
| ) -> list[Request]: | |
| # Check if we have any prompts (from command line or JSONL) | |
| if args.prompts: | |
| if args.prompt_file: | |
| raise ValueError("Cannot use both --prompts and --prompt-file") | |
| return get_cli_requests(args, tokenizer, sampling_params) | |
| elif args.prompt_file: | |
| return get_requests_from_file(args, tokenizer, sampling_params) | |
| else: | |
| return get_synthetic_requests(args, tokenizer, sampling_params) | |
| def get_model_size_str(model): | |
| n = sum(p.numel() for p in model.parameters()) | |
| for exp, suffix in ((12, "t"), (9, "b"), (6, "m"), (3, "k"), (0, "")): | |
| nquery = int(10**exp) | |
| if n > nquery: | |
| return "%d%s" % (n // nquery, suffix) | |
| raise Exception("something went wrong.") | |
| def build_dynamic_engine_setup_prefix( | |
| args: Namespace, | |
| model: MegatronModule, | |
| context: DynamicInferenceContext, | |
| requests: list[DynamicInferenceRequest], | |
| ): | |
| """ | |
| Returns a compact, pipe-separated summary of the dynamic-batching setup. | |
| Example output: | |
| `dynamic | cg True | prompts: synth(16 256), n 1024, g 512, t 1.0e+02 5.0e-01 | bf 4, 1.2 [r 1024, t 8192] | gtd 0.50 [r 512] | reqs 100` # pylint: disable=line-too-long | |
| Args: | |
| args (Namespace): Command-line arguments for this run. | |
| context (DynamicInferenceContext): Stores limits such as `max_requests`, | |
| `max_tokens`, and `gtd_request_count`. | |
| requests (List[DynamicInferenceRequest]): List of inference requests. | |
| Returns: | |
| A configuration string for logging. | |
| """ | |
| # CUDA graph config | |
| if args.cuda_graph_impl == "local": | |
| cg_str = f"graphs {len(context.cuda_graph_batch_dimensions_list)}" | |
| else: | |
| cg_str = "--" | |
| # Unified memory (UVM). | |
| uvm_str = f"uvm {int(context.unified_memory_level)}" | |
| # Prompt description | |
| prompt_src_str = ( | |
| "cli" if args.prompts else | |
| "file" if args.prompt_file else | |
| f"synth({', '.join(map(str, args.num_tokens_to_prompt))})" | |
| ) | |
| request_str = ( | |
| f"requests: {prompt_src_str}, " | |
| f"n {len(requests):d}, g {args.num_tokens_to_generate:d}, " | |
| ) | |
| request_str += ( | |
| f"dur {args.incoming_requests_duration:.1e} " | |
| f"r/sec {args.incoming_requests_per_sec:.1e}" | |
| if args.incoming_requests_per_step is None else | |
| f"r/step {args.incoming_requests_per_step}" | |
| ) | |
| # Buffer limits config | |
| buffer_limits_str = ( | |
| f"bf: {get_mem_size_str(args.inference_dynamic_batching_buffer_size_gb*1024**3)}, " | |
| f"{context.block_allocator.active_count} chunks " | |
| f"[r {context.max_requests}, t {context.max_tokens}]" | |
| ) | |
| parts = [ | |
| get_model_size_str(model), | |
| "dynamic", | |
| cg_str, | |
| uvm_str, | |
| request_str, | |
| buffer_limits_str, | |
| ] | |
| return " | ".join(parts) | |
| def get_global_peak_memory_stats_bytes() -> dict: | |
| """Peak allocated CUDA memory aggregated across ranks (MAX), in bytes. | |
| Uses `torch.cuda.max_memory_allocated()` and assumes peak stats were reset | |
| before the benchmark run. | |
| """ | |
| peak_alloc = int(torch.cuda.max_memory_allocated()) | |
| if torch.distributed.is_available() and torch.distributed.is_initialized(): | |
| t = torch.tensor([peak_alloc], device="cuda", dtype=torch.int64) | |
| torch.distributed.all_reduce(t, op=torch.distributed.ReduceOp.MAX) | |
| peak_alloc = int(t[0].item()) | |
| return {"mem-max-allocated-bytes": peak_alloc} |