1ripon1's picture
Upload folder using huggingface_hub
7344bef verified
Raw
History Blame Contribute Delete
14.9 kB
import atexit
from dataclasses import fields
from time import perf_counter
from tqdm.auto import tqdm
from transformers import AutoTokenizer
import torch.multiprocessing as mp
from ..config import Config
from ..sampling_params import SamplingParams
from .sequence import Sequence
from .scheduler import Scheduler
from .block_manager import BlockManager
from .model_runner import ModelRunner
class LLMEngine:
def __init__(self, model, **kwargs):
model_object = kwargs.get("model_object", None)
graph_pool_handle = kwargs.get("graph_pool_handle", None)
config_fields = {field.name for field in fields(Config)}
config_kwargs = {k: v for k, v in kwargs.items() if k in config_fields}
config = Config(model, **config_kwargs)
self.config = config
self.ps = []
self.events = []
ctx = mp.get_context("spawn")
for i in range(1, config.tensor_parallel_size):
event = ctx.Event()
process = ctx.Process(target=ModelRunner, args=(config, i, event))
process.start()
self.ps.append(process)
self.events.append(event)
self.model_runner = ModelRunner(config, 0, self.events, model_object=model_object, graph_pool_handle=graph_pool_handle)
tokenizer = kwargs.get("tokenizer", None)
if tokenizer is not None:
self.tokenizer = tokenizer
else:
self.tokenizer = AutoTokenizer.from_pretrained(config.model, use_fast=True)
config.eos = self.tokenizer.eos_token_id
self.scheduler = Scheduler(config)
self._exit_registered = False
self._closed = False
self._exited = False
self._atexit_callback = self.exit
atexit.register(self._atexit_callback)
self._exit_registered = True
def exit(self):
if self._exited:
return
self._exited = True
runner = getattr(self, "model_runner", None)
if runner is not None:
try:
runner.call("exit")
except Exception:
pass
try:
del self.model_runner
except Exception:
pass
for p in list(getattr(self, "ps", [])):
try:
p.join()
except Exception:
pass
self.ps = []
self.events = []
def close(self):
if self._closed:
return
self._closed = True
try:
self.reset_runtime_state()
except Exception:
pass
try:
self.clear_graph_cache()
except Exception:
pass
try:
self.exit()
except Exception:
pass
if self._exit_registered:
try:
atexit.unregister(self._atexit_callback)
except Exception:
pass
self._exit_registered = False
self._atexit_callback = None
self.scheduler = None
self.tokenizer = None
def __del__(self):
try:
self.close()
except Exception:
pass
def reset_runtime_state(self):
runner = getattr(self, "model_runner", None)
if runner is None:
return
runner.reset_runtime_state()
# KV cache is invalid after runtime reset/reprepare, so cached prefix block metadata
# must be dropped as well to prevent stale-cache reuse.
try:
self.reset()
except Exception:
pass
if self.scheduler is None:
return
self.scheduler.waiting.clear()
self.scheduler.running.clear()
self.scheduler.block_manager = BlockManager(
self.config.num_kvcache_blocks,
self.config.kvcache_block_size,
)
def clear_graph_cache(self):
runner = getattr(self, "model_runner", None)
if runner is None:
return
runner.clear_graph_cache()
def add_request(
self,
prompt: str | list[int],
sampling_params: SamplingParams,
unconditional_prompt: str | list[int] | None = None,
prompt_embeds=None,
prompt_position_ids=None,
position_offset: int = 0,
unconditional_prompt_embeds=None,
unconditional_prompt_position_ids=None,
unconditional_position_offset: int = 0,
):
if isinstance(prompt, str):
prompt = self.tokenizer.encode(prompt)
# For CFG: if cfg_scale > 1.0, create both conditional and unconditional sequences
if sampling_params.cfg_scale > 1.0:
if unconditional_prompt is None:
# Try to construct unconditional prompt by replacing user input with "NO USER INPUT"
# This is a fallback - ideally users should provide unconditional_prompt
if isinstance(prompt, list):
# For now, just use the same prompt (user should provide unconditional_prompt)
# TODO: Implement automatic "NO USER INPUT" replacement if possible
unconditional_prompt = prompt
else:
unconditional_prompt = prompt
if isinstance(unconditional_prompt, str):
unconditional_prompt = self.tokenizer.encode(unconditional_prompt)
# Create unconditional sequence first (so we can reference it from conditional)
uncond_seq = Sequence(
unconditional_prompt,
sampling_params,
is_unconditional=True,
prompt_embeds=unconditional_prompt_embeds,
prompt_position_ids=unconditional_prompt_position_ids,
position_offset=unconditional_position_offset,
)
# Create conditional sequence with reference to unconditional
cond_seq = Sequence(
prompt,
sampling_params,
is_unconditional=False,
conditional_seq=uncond_seq,
prompt_embeds=prompt_embeds,
prompt_position_ids=prompt_position_ids,
position_offset=position_offset,
)
uncond_seq.paired_seq = cond_seq # Link them bidirectionally
# Add both sequences to scheduler
self.scheduler.add(cond_seq)
self.scheduler.add(uncond_seq)
else:
seq = Sequence(
prompt,
sampling_params,
prompt_embeds=prompt_embeds,
prompt_position_ids=prompt_position_ids,
position_offset=position_offset,
)
self.scheduler.add(seq)
def step(self):
seqs, is_prefill = self.scheduler.schedule()
token_ids = self.model_runner.call("run", seqs, is_prefill)
self.scheduler.postprocess(seqs, token_ids)
# Only output conditional sequences (unconditional sequences are just for CFG computation)
output_seqs = [seq for seq in seqs if seq.is_finished and (seq.cfg_scale <= 1.0 or not seq.is_unconditional)]
outputs = [(seq.seq_id, seq.completion_token_ids) for seq in output_seqs]
num_tokens = sum(len(seq) for seq in seqs) if is_prefill else -len([s for s in seqs if not s.is_unconditional])
return outputs, num_tokens
def is_finished(self):
if self.scheduler is None:
return True
return self.scheduler.is_finished()
def reset(self):
"""
Reset the scheduler state and release all allocated blocks.
This should be called when an exception occurs during generation to prevent
KV cache block leaks that can cause 'deque index out of range' errors.
"""
# Deallocate all running sequences
if self.scheduler is None:
return
while self.scheduler.running:
seq = self.scheduler.running.popleft()
if seq.block_table: # Only deallocate if blocks are allocated
self.scheduler.block_manager.deallocate(seq)
# Deallocate all waiting sequences (they might have blocks from preemption)
while self.scheduler.waiting:
seq = self.scheduler.waiting.popleft()
if seq.block_table:
self.scheduler.block_manager.deallocate(seq)
def generate(
self,
prompts: list[str] | list[list[int]],
sampling_params: SamplingParams | list[SamplingParams],
use_tqdm: bool = True,
unconditional_prompts: list[str] | list[list[int]] | None = None,
) -> list[str]:
if self.scheduler is None:
raise RuntimeError("LLM engine is closed.")
# Ensure model runtime/KV cache are prepared, and sync scheduler blocks.
self.model_runner.ensure_runtime_ready()
if (self.config.num_kvcache_blocks > 0 and
len(self.scheduler.block_manager.blocks) != self.config.num_kvcache_blocks):
self.scheduler.block_manager = BlockManager(
self.config.num_kvcache_blocks,
self.config.kvcache_block_size,
)
# Clean up any residual state from previous interrupted generations
# This prevents 'deque index out of range' errors from accumulated block leaks
if not self.is_finished():
self.reset()
self.model_runner.reset_generation_state()
if use_tqdm:
pbar = tqdm(total=len(prompts), desc="Generating", dynamic_ncols=True)
if not isinstance(sampling_params, list):
sampling_params = [sampling_params] * len(prompts)
# Seed once per request-batch; keeps deterministic decode without per-step overhead.
seed_to_apply = None
for sp in sampling_params:
seed_val = getattr(sp, "seed", None)
if seed_val is not None:
try:
seed_to_apply = int(seed_val)
break
except Exception:
seed_to_apply = None
self.model_runner.call("set_sampling_seed", seed_to_apply)
if unconditional_prompts is None:
unconditional_prompts = [None] * len(prompts)
for prompt, sp, uncond_prompt in zip(prompts, sampling_params, unconditional_prompts):
self.add_request(prompt, sp, uncond_prompt)
outputs = {}
prefill_throughput = decode_throughput = 0.
try:
while not self.is_finished():
t = perf_counter()
output, num_tokens = self.step()
if use_tqdm:
if num_tokens > 0:
prefill_throughput = num_tokens / (perf_counter() - t)
else:
decode_throughput = -num_tokens / (perf_counter() - t)
pbar.set_postfix({
"Prefill": f"{int(prefill_throughput)}tok/s",
"Decode": f"{int(decode_throughput)}tok/s",
})
for seq_id, token_ids in output:
outputs[seq_id] = token_ids
if use_tqdm:
pbar.update(1)
except Exception:
# Clean up on exception to prevent block leaks
self.reset()
raise
finally:
if use_tqdm:
pbar.close()
outputs = [outputs[seq_id] for seq_id in sorted(outputs.keys())]
outputs = [{"text": self.tokenizer.decode(token_ids), "token_ids": token_ids} for token_ids in outputs]
return outputs
def generate_embedded(
self,
prompts: list[list[int]],
prompt_embeds: list,
prompt_position_ids: list | None,
sampling_params: SamplingParams | list[SamplingParams],
position_offsets: list[int] | None = None,
use_tqdm: bool = True,
):
if self.scheduler is None:
raise RuntimeError("LLM engine is closed.")
self.model_runner.ensure_runtime_ready()
if (
self.config.num_kvcache_blocks > 0
and len(self.scheduler.block_manager.blocks) != self.config.num_kvcache_blocks
):
self.scheduler.block_manager = BlockManager(
self.config.num_kvcache_blocks,
self.config.kvcache_block_size,
)
if not self.is_finished():
self.reset()
self.model_runner.reset_generation_state()
if not isinstance(sampling_params, list):
sampling_params = [sampling_params] * len(prompts)
if prompt_position_ids is None:
prompt_position_ids = [None] * len(prompts)
if position_offsets is None:
position_offsets = [0] * len(prompts)
if use_tqdm:
pbar = tqdm(total=len(prompts), desc="Generating", dynamic_ncols=True)
seed_to_apply = None
for sp in sampling_params:
seed_val = getattr(sp, "seed", None)
if seed_val is not None:
try:
seed_to_apply = int(seed_val)
break
except Exception:
seed_to_apply = None
self.model_runner.call("set_sampling_seed", seed_to_apply)
for prompt, embeds, pos_ids, pos_offset, sp in zip(prompts, prompt_embeds, prompt_position_ids, position_offsets, sampling_params):
self.add_request(
prompt,
sp,
prompt_embeds=embeds,
prompt_position_ids=pos_ids,
position_offset=pos_offset,
)
outputs = {}
prefill_throughput = decode_throughput = 0.0
try:
while not self.is_finished():
t = perf_counter()
output, num_tokens = self.step()
if use_tqdm:
if num_tokens > 0:
prefill_throughput = num_tokens / (perf_counter() - t)
else:
decode_throughput = -num_tokens / (perf_counter() - t)
pbar.set_postfix({
"Prefill": f"{int(prefill_throughput)}tok/s",
"Decode": f"{int(decode_throughput)}tok/s",
})
for seq_id, token_ids in output:
outputs[seq_id] = token_ids
if use_tqdm:
pbar.update(1)
except Exception:
self.reset()
raise
finally:
if use_tqdm:
pbar.close()
outputs = [outputs[seq_id] for seq_id in sorted(outputs.keys())]
outputs = [{"text": self.tokenizer.decode(token_ids), "token_ids": token_ids} for token_ids in outputs]
return outputs