mini-infer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mini_infer/__init__.py +9 -0
- mini_infer/cache/__init__.py +4 -0
- mini_infer/cache/block_pool.py +120 -0
- mini_infer/cache/prefix_cache.py +86 -0
- mini_infer/cli.py +77 -0
- mini_infer/engine.py +155 -0
- mini_infer/models/__init__.py +5 -0
- mini_infer/models/config.py +68 -0
- mini_infer/models/loader.py +122 -0
- mini_infer/models/qwen2.py +276 -0
- mini_infer/models/sampler.py +62 -0
- mini_infer/scheduler/__init__.py +3 -0
- mini_infer/scheduler/continuous.py +127 -0
- mini_infer/server.py +194 -0
- mini_infer/speculative/__init__.py +3 -0
- mini_infer/speculative/verifier.py +126 -0
- mini_infer-0.1.0.dist-info/METADATA +378 -0
- mini_infer-0.1.0.dist-info/RECORD +21 -0
- mini_infer-0.1.0.dist-info/WHEEL +4 -0
- mini_infer-0.1.0.dist-info/entry_points.txt +2 -0
- mini_infer-0.1.0.dist-info/licenses/LICENSE +201 -0
mini_infer/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""mini-infer: Minimal continuous-batching inference engine with paged KV-cache."""
|
|
2
|
+
|
|
3
|
+
from mini_infer.engine import InferenceEngine
|
|
4
|
+
from mini_infer.models.config import Qwen2Config
|
|
5
|
+
from mini_infer.models.qwen2 import Qwen2ForCausalLM
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
__all__ = ["InferenceEngine", "Qwen2Config", "Qwen2ForCausalLM"]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BlockPool:
|
|
5
|
+
"""Fixed-size GPU memory pool for Paged KV-Cache blocks.
|
|
6
|
+
|
|
7
|
+
Instead of allocating a large contiguous buffer per request, the engine pre-allocates
|
|
8
|
+
a single fixed pool of blocks on the GPU and maps requests dynamically via a block table.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
num_blocks: int,
|
|
14
|
+
block_size: int,
|
|
15
|
+
num_layers: int,
|
|
16
|
+
num_kv_heads: int,
|
|
17
|
+
head_dim: int,
|
|
18
|
+
device: torch.device | None = None,
|
|
19
|
+
dtype: torch.dtype | None = None,
|
|
20
|
+
):
|
|
21
|
+
if device is None:
|
|
22
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
23
|
+
if dtype is None:
|
|
24
|
+
dtype = torch.float16 if torch.cuda.is_available() else torch.float32
|
|
25
|
+
self.num_blocks = num_blocks
|
|
26
|
+
self.block_size = block_size
|
|
27
|
+
self.num_layers = num_layers
|
|
28
|
+
self.num_kv_heads = num_kv_heads
|
|
29
|
+
self.head_dim = head_dim
|
|
30
|
+
self.device = device
|
|
31
|
+
self.dtype = dtype
|
|
32
|
+
|
|
33
|
+
# Free list of physical block indices
|
|
34
|
+
self.free_list: list[int] = list(range(num_blocks))
|
|
35
|
+
|
|
36
|
+
# Reference count per block for prefix caching / copy-on-write
|
|
37
|
+
self.ref_counts: dict[int, int] = {i: 0 for i in range(num_blocks)}
|
|
38
|
+
|
|
39
|
+
# Pre-allocate physical KV tensors per layer
|
|
40
|
+
# Shape per layer: [num_blocks, num_kv_heads, block_size, head_dim]
|
|
41
|
+
self.key_caches: list[torch.Tensor] = [
|
|
42
|
+
torch.zeros((num_blocks, num_kv_heads, block_size, head_dim), device=device, dtype=dtype)
|
|
43
|
+
for _ in range(num_layers)
|
|
44
|
+
]
|
|
45
|
+
self.value_caches: list[torch.Tensor] = [
|
|
46
|
+
torch.zeros((num_blocks, num_kv_heads, block_size, head_dim), device=device, dtype=dtype)
|
|
47
|
+
for _ in range(num_layers)
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def num_free(self) -> int:
|
|
52
|
+
"""Number of currently available unallocated physical blocks."""
|
|
53
|
+
return len(self.free_list)
|
|
54
|
+
|
|
55
|
+
def allocate_block(self) -> int:
|
|
56
|
+
"""Allocate a single physical block from the free list."""
|
|
57
|
+
if not self.free_list:
|
|
58
|
+
raise MemoryError("Paged KV-Cache pool exhausted! Out of GPU memory blocks.")
|
|
59
|
+
block_id = self.free_list.pop()
|
|
60
|
+
self.ref_counts[block_id] = 1
|
|
61
|
+
return block_id
|
|
62
|
+
|
|
63
|
+
def allocate_blocks(self, count: int) -> list[int]:
|
|
64
|
+
"""Allocate multiple physical blocks."""
|
|
65
|
+
if len(self.free_list) < count:
|
|
66
|
+
raise MemoryError(
|
|
67
|
+
f"Cannot allocate {count} blocks; only {len(self.free_list)} available."
|
|
68
|
+
)
|
|
69
|
+
allocated = []
|
|
70
|
+
for _ in range(count):
|
|
71
|
+
allocated.append(self.allocate_block())
|
|
72
|
+
return allocated
|
|
73
|
+
|
|
74
|
+
def retain_block(self, block_id: int):
|
|
75
|
+
"""Increment reference count for a block (used in prefix caching)."""
|
|
76
|
+
self.ref_counts[block_id] += 1
|
|
77
|
+
|
|
78
|
+
def free_block(self, block_id: int):
|
|
79
|
+
"""Decrement reference count; return to free list if count reaches 0."""
|
|
80
|
+
self.ref_counts[block_id] -= 1
|
|
81
|
+
if self.ref_counts[block_id] <= 0:
|
|
82
|
+
self.ref_counts[block_id] = 0
|
|
83
|
+
self.free_list.append(block_id)
|
|
84
|
+
|
|
85
|
+
def free_blocks(self, block_ids: list[int]):
|
|
86
|
+
"""Free a list of blocks."""
|
|
87
|
+
for b_id in block_ids:
|
|
88
|
+
self.free_block(b_id)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class BlockTable:
|
|
92
|
+
"""Logical-to-physical block mapping for an active request sequence."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, block_size: int = 16):
|
|
95
|
+
self.block_size = block_size
|
|
96
|
+
self.physical_block_ids: list[int] = []
|
|
97
|
+
self.num_tokens: int = 0
|
|
98
|
+
|
|
99
|
+
def append_slot(self, block_pool: BlockPool) -> tuple[int, int]:
|
|
100
|
+
"""Reserve the next token slot.
|
|
101
|
+
|
|
102
|
+
Allocates a new physical block from the pool if the current block is full.
|
|
103
|
+
Returns:
|
|
104
|
+
Tuple of (physical_block_id, slot_offset_within_block).
|
|
105
|
+
"""
|
|
106
|
+
slot_offset = self.num_tokens % self.block_size
|
|
107
|
+
if slot_offset == 0:
|
|
108
|
+
# Need a new physical block
|
|
109
|
+
new_block_id = block_pool.allocate_block()
|
|
110
|
+
self.physical_block_ids.append(new_block_id)
|
|
111
|
+
|
|
112
|
+
current_block_id = self.physical_block_ids[-1]
|
|
113
|
+
self.num_tokens += 1
|
|
114
|
+
return current_block_id, slot_offset
|
|
115
|
+
|
|
116
|
+
def free(self, block_pool: BlockPool):
|
|
117
|
+
"""Free all physical blocks associated with this request."""
|
|
118
|
+
block_pool.free_blocks(self.physical_block_ids)
|
|
119
|
+
self.physical_block_ids.clear()
|
|
120
|
+
self.num_tokens = 0
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from mini_infer.cache.block_pool import BlockPool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PrefixCache:
|
|
7
|
+
"""Radix / Hash-based Prefix Cache for zero-recompute prompt KV reuse.
|
|
8
|
+
|
|
9
|
+
Stores and matches chains of fixed-size token blocks so that common prefixes
|
|
10
|
+
(system instructions, multi-turn chat history, few-shot examples) bypass prefill
|
|
11
|
+
and reuse pre-computed KV tensors directly from GPU VRAM.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, block_size: int = 16):
|
|
15
|
+
self.block_size = block_size
|
|
16
|
+
# Maps chained block tuple hash -> physical_block_id
|
|
17
|
+
self.cache: dict[tuple[int, ...], int] = {}
|
|
18
|
+
# Tracks last access time for LRU eviction: physical_block_id -> timestamp
|
|
19
|
+
self.last_accessed: dict[int, float] = {}
|
|
20
|
+
|
|
21
|
+
def match_prefix(
|
|
22
|
+
self, token_ids: list[int], block_pool: BlockPool
|
|
23
|
+
) -> tuple[list[int], int]:
|
|
24
|
+
"""Find the longest cached block sequence that matches the start of token_ids.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
token_ids: Full prompt token sequence.
|
|
28
|
+
block_pool: Global block pool to retain matched blocks.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Tuple of (matched_physical_block_ids, num_cached_tokens).
|
|
32
|
+
"""
|
|
33
|
+
matched_blocks = []
|
|
34
|
+
num_full_blocks = len(token_ids) // self.block_size
|
|
35
|
+
|
|
36
|
+
prefix_tokens: list[int] = []
|
|
37
|
+
for i in range(num_full_blocks):
|
|
38
|
+
chunk = tuple(token_ids[i * self.block_size : (i + 1) * self.block_size])
|
|
39
|
+
prefix_tokens.extend(chunk)
|
|
40
|
+
key = tuple(prefix_tokens)
|
|
41
|
+
|
|
42
|
+
if key in self.cache:
|
|
43
|
+
physical_block_id = self.cache[key]
|
|
44
|
+
# Retain in memory pool
|
|
45
|
+
block_pool.retain_block(physical_block_id)
|
|
46
|
+
matched_blocks.append(physical_block_id)
|
|
47
|
+
self.last_accessed[physical_block_id] = time.time()
|
|
48
|
+
else:
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
num_cached_tokens = len(matched_blocks) * self.block_size
|
|
52
|
+
return matched_blocks, num_cached_tokens
|
|
53
|
+
|
|
54
|
+
def insert_prefix_block(
|
|
55
|
+
self,
|
|
56
|
+
full_prefix_tokens_up_to_block: tuple[int, ...],
|
|
57
|
+
physical_block_id: int,
|
|
58
|
+
block_pool: BlockPool,
|
|
59
|
+
):
|
|
60
|
+
"""Register a newly computed full block into the prefix cache."""
|
|
61
|
+
assert len(full_prefix_tokens_up_to_block) % self.block_size == 0
|
|
62
|
+
key = tuple(full_prefix_tokens_up_to_block)
|
|
63
|
+
|
|
64
|
+
if key not in self.cache:
|
|
65
|
+
self.cache[key] = physical_block_id
|
|
66
|
+
block_pool.retain_block(physical_block_id)
|
|
67
|
+
self.last_accessed[physical_block_id] = time.time()
|
|
68
|
+
|
|
69
|
+
def evict_lru(self, block_pool: BlockPool, count: int = 1) -> int:
|
|
70
|
+
"""Evict the least recently used cached prefix blocks when VRAM is tight."""
|
|
71
|
+
evicted = 0
|
|
72
|
+
# Sort by access timestamp ascending
|
|
73
|
+
sorted_blocks = sorted(self.last_accessed.items(), key=lambda x: x[1])
|
|
74
|
+
|
|
75
|
+
for block_id, _ in sorted_blocks:
|
|
76
|
+
if evicted >= count:
|
|
77
|
+
break
|
|
78
|
+
# Find the cache key
|
|
79
|
+
keys_to_delete = [k for k, v in self.cache.items() if v == block_id]
|
|
80
|
+
for k in keys_to_delete:
|
|
81
|
+
del self.cache[k]
|
|
82
|
+
del self.last_accessed[block_id]
|
|
83
|
+
block_pool.free_block(block_id)
|
|
84
|
+
evicted += 1
|
|
85
|
+
|
|
86
|
+
return evicted
|
mini_infer/cli.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
app = typer.Typer(
|
|
5
|
+
name="mini-infer",
|
|
6
|
+
help="Minimal continuous-batching LLM inference engine with paged KV-cache.",
|
|
7
|
+
no_args_is_help=True,
|
|
8
|
+
)
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def serve(
|
|
14
|
+
model: str = typer.Option("Qwen/Qwen2.5-0.5B-Instruct", "--model", "-m", help="Model path or HuggingFace repo ID"),
|
|
15
|
+
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host address to bind to"),
|
|
16
|
+
port: int = typer.Option(8000, "--port", "-p", help="Port to listen on"),
|
|
17
|
+
num_blocks: int = typer.Option(256, "--num-blocks", help="Number of physical Paged KV blocks"),
|
|
18
|
+
block_size: int = typer.Option(16, "--block-size", help="Tokens per physical block"),
|
|
19
|
+
device: str = typer.Option("auto", "--device", help="Device (cuda/cpu/auto)"),
|
|
20
|
+
):
|
|
21
|
+
"""Launch the OpenAI-compatible streaming HTTP server."""
|
|
22
|
+
import torch
|
|
23
|
+
import uvicorn
|
|
24
|
+
from transformers import AutoTokenizer
|
|
25
|
+
|
|
26
|
+
from mini_infer import server
|
|
27
|
+
from mini_infer.engine import InferenceEngine
|
|
28
|
+
from mini_infer.models.loader import load_model_from_safetensors
|
|
29
|
+
|
|
30
|
+
selected_device = "cuda" if (device == "auto" and torch.cuda.is_available()) else ("cpu" if device == "auto" else device)
|
|
31
|
+
console.print(f"[bold cyan]mini-infer[/bold cyan] serving [bold green]{model}[/bold green] on [yellow]{host}:{port}[/yellow] ({selected_device})")
|
|
32
|
+
|
|
33
|
+
# Load tokenizer
|
|
34
|
+
console.print("Loading tokenizer...")
|
|
35
|
+
try:
|
|
36
|
+
server.tokenizer = AutoTokenizer.from_pretrained(model)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
console.print(f"[yellow]Warning: Could not load tokenizer ({e}). Using character fallback.[/yellow]")
|
|
39
|
+
|
|
40
|
+
# Load model
|
|
41
|
+
console.print("Loading model weights...")
|
|
42
|
+
model_instance, config = load_model_from_safetensors(model, device=selected_device)
|
|
43
|
+
|
|
44
|
+
# Initialize inference engine
|
|
45
|
+
console.print(f"Initializing Paged KV-Cache ({num_blocks} blocks of size {block_size})...")
|
|
46
|
+
server.engine = InferenceEngine(
|
|
47
|
+
model=model_instance,
|
|
48
|
+
config=config,
|
|
49
|
+
num_blocks=num_blocks,
|
|
50
|
+
block_size=block_size,
|
|
51
|
+
device=torch.device(selected_device),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
console.print(f"[bold green]Server ready![/bold green] Connect any OpenAI-compatible client or run [bold]servebench quick[/bold] against http://{host}:{port}/v1")
|
|
55
|
+
uvicorn.run(server.app, host=host, port=port, log_level="info")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command()
|
|
59
|
+
def info():
|
|
60
|
+
"""Display system hardware and GPU VRAM budget for mini-infer."""
|
|
61
|
+
import torch
|
|
62
|
+
|
|
63
|
+
console.print("[bold cyan]mini-infer System Diagnostics[/bold cyan]")
|
|
64
|
+
console.print(f"PyTorch version: {torch.__version__}")
|
|
65
|
+
cuda_avail = torch.cuda.is_available()
|
|
66
|
+
console.print(f"CUDA available: [{'green' if cuda_avail else 'red'}]{cuda_avail}[/]")
|
|
67
|
+
|
|
68
|
+
if cuda_avail:
|
|
69
|
+
device_name = torch.cuda.get_device_name(0)
|
|
70
|
+
total_vram_gb = torch.cuda.get_device_properties(0).total_memory / (1024 ** 3)
|
|
71
|
+
console.print(f"GPU Device: [bold green]{device_name}[/bold green]")
|
|
72
|
+
console.print(f"Total VRAM: [bold yellow]{total_vram_gb:.2f} GB[/bold yellow]")
|
|
73
|
+
console.print("\n[bold]Memory Allocation Plan (RTX 3050 6 GB):[/bold]")
|
|
74
|
+
console.print(" • Qwen2.5-1.5B (Target Model): ~3.0 GB VRAM")
|
|
75
|
+
console.print(" • Qwen2.5-0.5B (Draft Model): ~1.0 GB VRAM")
|
|
76
|
+
console.print(" • Paged KV-Cache Memory Pool: ~1.5 GB VRAM (~2000 blocks)")
|
|
77
|
+
console.print(" • Safety Buffer: ~0.5 GB")
|
mini_infer/engine.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from mini_infer.cache.block_pool import BlockPool
|
|
4
|
+
from mini_infer.cache.prefix_cache import PrefixCache
|
|
5
|
+
from mini_infer.models.config import Qwen2Config
|
|
6
|
+
from mini_infer.models.qwen2 import Qwen2ForCausalLM
|
|
7
|
+
from mini_infer.models.sampler import sample_next_token
|
|
8
|
+
from mini_infer.scheduler.continuous import (
|
|
9
|
+
ContinuousScheduler,
|
|
10
|
+
Sequence,
|
|
11
|
+
SequenceStatus,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InferenceEngine:
|
|
16
|
+
"""End-to-End LLM Serving Engine combining Model, Paged KV-Cache, Prefix Cache, and Continuous Batching."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
model: Qwen2ForCausalLM,
|
|
21
|
+
config: Qwen2Config,
|
|
22
|
+
num_blocks: int = 256,
|
|
23
|
+
block_size: int = 16,
|
|
24
|
+
max_batch_size: int = 8,
|
|
25
|
+
enable_prefix_caching: bool = True,
|
|
26
|
+
device: torch.device | None = None,
|
|
27
|
+
):
|
|
28
|
+
if device is None:
|
|
29
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
30
|
+
self.model = model
|
|
31
|
+
self.config = config
|
|
32
|
+
self.device = device
|
|
33
|
+
self.block_size = block_size
|
|
34
|
+
self.enable_prefix_caching = enable_prefix_caching
|
|
35
|
+
|
|
36
|
+
# 1. Paged KV memory pool
|
|
37
|
+
self.block_pool = BlockPool(
|
|
38
|
+
num_blocks=num_blocks,
|
|
39
|
+
block_size=block_size,
|
|
40
|
+
num_layers=config.num_hidden_layers,
|
|
41
|
+
num_kv_heads=config.num_key_value_heads,
|
|
42
|
+
head_dim=config.head_dim,
|
|
43
|
+
device=device,
|
|
44
|
+
dtype=next(model.parameters()).dtype,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# 2. Prefix Cache (Radix / Hash based)
|
|
48
|
+
self.prefix_cache = PrefixCache(block_size=block_size) if enable_prefix_caching else None
|
|
49
|
+
|
|
50
|
+
# 3. Continuous Batching Scheduler
|
|
51
|
+
self.scheduler = ContinuousScheduler(
|
|
52
|
+
block_pool=self.block_pool,
|
|
53
|
+
max_batch_size=max_batch_size,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def add_request(
|
|
57
|
+
self,
|
|
58
|
+
prompt_token_ids: list[int],
|
|
59
|
+
max_tokens: int = 128,
|
|
60
|
+
temperature: float = 0.0,
|
|
61
|
+
top_p: float = 1.0,
|
|
62
|
+
stop_token_ids: set[int] | None = None,
|
|
63
|
+
request_id: str | None = None,
|
|
64
|
+
) -> Sequence:
|
|
65
|
+
"""Submit a new request to the waiting queue."""
|
|
66
|
+
seq = Sequence(
|
|
67
|
+
prompt_token_ids=prompt_token_ids,
|
|
68
|
+
max_tokens=max_tokens,
|
|
69
|
+
temperature=temperature,
|
|
70
|
+
top_p=top_p,
|
|
71
|
+
stop_token_ids=stop_token_ids,
|
|
72
|
+
request_id=request_id,
|
|
73
|
+
)
|
|
74
|
+
self.scheduler.add_sequence(seq)
|
|
75
|
+
return seq
|
|
76
|
+
|
|
77
|
+
def step(self) -> list[tuple[Sequence, int]]:
|
|
78
|
+
"""Execute a single continuous batching iteration step.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of (sequence, newly_generated_token_id) for each active request in this step.
|
|
82
|
+
"""
|
|
83
|
+
prefill_seqs, decode_seqs = self.scheduler.schedule()
|
|
84
|
+
step_outputs: list[tuple[Sequence, int]] = []
|
|
85
|
+
|
|
86
|
+
# 1. Handle Prefill Sequences (newly admitted requests)
|
|
87
|
+
for seq in prefill_seqs:
|
|
88
|
+
input_tokens = seq.prompt_token_ids
|
|
89
|
+
|
|
90
|
+
# Prefix caching check
|
|
91
|
+
cached_blocks = []
|
|
92
|
+
num_cached = 0
|
|
93
|
+
if self.prefix_cache:
|
|
94
|
+
cached_blocks, num_cached = self.prefix_cache.match_prefix(input_tokens, self.block_pool)
|
|
95
|
+
seq.block_table.physical_block_ids.extend(cached_blocks)
|
|
96
|
+
seq.block_table.num_tokens = num_cached
|
|
97
|
+
|
|
98
|
+
# Uncached prompt tokens that must be computed
|
|
99
|
+
uncached_tokens = input_tokens[num_cached:]
|
|
100
|
+
if not uncached_tokens:
|
|
101
|
+
# Prompt was 100% cached!
|
|
102
|
+
uncached_tokens = [input_tokens[-1]]
|
|
103
|
+
seq.block_table.num_tokens -= 1
|
|
104
|
+
|
|
105
|
+
# Allocate slots for uncached tokens
|
|
106
|
+
for _ in uncached_tokens:
|
|
107
|
+
seq.block_table.append_slot(self.block_pool)
|
|
108
|
+
|
|
109
|
+
# Model forward pass for prefill
|
|
110
|
+
input_tensor = torch.tensor([uncached_tokens], dtype=torch.long, device=self.device)
|
|
111
|
+
with torch.no_grad():
|
|
112
|
+
logits, past_kv = self.model(input_tensor)
|
|
113
|
+
seq.past_key_values = past_kv
|
|
114
|
+
|
|
115
|
+
# Sample first output token
|
|
116
|
+
next_token = sample_next_token(
|
|
117
|
+
logits[:, -1, :],
|
|
118
|
+
temperature=seq.temperature,
|
|
119
|
+
top_p=seq.top_p,
|
|
120
|
+
).item()
|
|
121
|
+
|
|
122
|
+
seq.append_token(next_token)
|
|
123
|
+
step_outputs.append((seq, next_token))
|
|
124
|
+
|
|
125
|
+
# Register completed prompt blocks into prefix cache
|
|
126
|
+
if self.prefix_cache:
|
|
127
|
+
full_blocks_count = len(input_tokens) // self.block_size
|
|
128
|
+
for b_idx in range(len(cached_blocks), full_blocks_count):
|
|
129
|
+
block_id = seq.block_table.physical_block_ids[b_idx]
|
|
130
|
+
prefix_chunk = tuple(input_tokens[: (b_idx + 1) * self.block_size])
|
|
131
|
+
self.prefix_cache.insert_prefix_block(prefix_chunk, block_id, self.block_pool)
|
|
132
|
+
|
|
133
|
+
# 2. Handle Decode Sequences (all running streams generate 1 token concurrently)
|
|
134
|
+
for seq in decode_seqs:
|
|
135
|
+
if seq.status == SequenceStatus.FINISHED:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
last_token = seq.output_token_ids[-1]
|
|
139
|
+
seq.block_table.append_slot(self.block_pool)
|
|
140
|
+
|
|
141
|
+
input_tensor = torch.tensor([[last_token]], dtype=torch.long, device=self.device)
|
|
142
|
+
with torch.no_grad():
|
|
143
|
+
logits, past_kv = self.model(input_tensor, past_key_values=seq.past_key_values)
|
|
144
|
+
seq.past_key_values = past_kv
|
|
145
|
+
|
|
146
|
+
next_token = sample_next_token(
|
|
147
|
+
logits[:, -1, :],
|
|
148
|
+
temperature=seq.temperature,
|
|
149
|
+
top_p=seq.top_p,
|
|
150
|
+
).item()
|
|
151
|
+
|
|
152
|
+
seq.append_token(next_token)
|
|
153
|
+
step_outputs.append((seq, next_token))
|
|
154
|
+
|
|
155
|
+
return step_outputs
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Qwen2Config:
|
|
6
|
+
"""Configuration class for Qwen2.5 model architectures."""
|
|
7
|
+
vocab_size: int = 151936
|
|
8
|
+
hidden_size: int = 896
|
|
9
|
+
intermediate_size: int = 4864
|
|
10
|
+
num_hidden_layers: int = 24
|
|
11
|
+
num_attention_heads: int = 14
|
|
12
|
+
num_key_value_heads: int = 2
|
|
13
|
+
head_dim: int = 64
|
|
14
|
+
rms_norm_eps: float = 1e-6
|
|
15
|
+
rope_theta: float = 1000000.0
|
|
16
|
+
max_position_embeddings: int = 32768
|
|
17
|
+
tie_word_embeddings: bool = True
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def qwen2_5_0_5b(cls) -> "Qwen2Config":
|
|
21
|
+
"""Default preset for Qwen2.5-0.5B-Instruct."""
|
|
22
|
+
return cls(
|
|
23
|
+
vocab_size=151936,
|
|
24
|
+
hidden_size=896,
|
|
25
|
+
intermediate_size=4864,
|
|
26
|
+
num_hidden_layers=24,
|
|
27
|
+
num_attention_heads=14,
|
|
28
|
+
num_key_value_heads=2,
|
|
29
|
+
head_dim=64,
|
|
30
|
+
rms_norm_eps=1e-6,
|
|
31
|
+
rope_theta=1000000.0,
|
|
32
|
+
max_position_embeddings=32768,
|
|
33
|
+
tie_word_embeddings=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def qwen2_5_1_5b(cls) -> "Qwen2Config":
|
|
38
|
+
"""Default preset for Qwen2.5-1.5B-Instruct."""
|
|
39
|
+
return cls(
|
|
40
|
+
vocab_size=151936,
|
|
41
|
+
hidden_size=1536,
|
|
42
|
+
intermediate_size=8960,
|
|
43
|
+
num_hidden_layers=28,
|
|
44
|
+
num_attention_heads=12,
|
|
45
|
+
num_key_value_heads=2,
|
|
46
|
+
head_dim=128,
|
|
47
|
+
rms_norm_eps=1e-6,
|
|
48
|
+
rope_theta=1000000.0,
|
|
49
|
+
max_position_embeddings=32768,
|
|
50
|
+
tie_word_embeddings=False,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_dict(cls, data: dict) -> "Qwen2Config":
|
|
55
|
+
"""Construct config from HuggingFace config.json dictionary."""
|
|
56
|
+
return cls(
|
|
57
|
+
vocab_size=data.get("vocab_size", 151936),
|
|
58
|
+
hidden_size=data.get("hidden_size", 896),
|
|
59
|
+
intermediate_size=data.get("intermediate_size", 4864),
|
|
60
|
+
num_hidden_layers=data.get("num_hidden_layers", 24),
|
|
61
|
+
num_attention_heads=data.get("num_attention_heads", 14),
|
|
62
|
+
num_key_value_heads=data.get("num_key_value_heads", 2),
|
|
63
|
+
head_dim=data.get("head_dim", data.get("hidden_size", 896) // data.get("num_attention_heads", 14)),
|
|
64
|
+
rms_norm_eps=data.get("rms_norm_eps", 1e-6),
|
|
65
|
+
rope_theta=data.get("rope_theta", 1000000.0),
|
|
66
|
+
max_position_embeddings=data.get("max_position_embeddings", 32768),
|
|
67
|
+
tie_word_embeddings=data.get("tie_word_embeddings", True),
|
|
68
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
from mini_infer.models.config import Qwen2Config
|
|
7
|
+
from mini_infer.models.qwen2 import Qwen2ForCausalLM
|
|
8
|
+
|
|
9
|
+
DT_MAP = {
|
|
10
|
+
"BF16": torch.bfloat16,
|
|
11
|
+
"F16": torch.float16,
|
|
12
|
+
"F32": torch.float32,
|
|
13
|
+
"I32": torch.int32,
|
|
14
|
+
"I64": torch.int64,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_safetensors_into_model(
|
|
19
|
+
model: torch.nn.Module,
|
|
20
|
+
sf_path: Path,
|
|
21
|
+
target_dtype: torch.dtype,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Stream weights directly into model parameters in-place to avoid duplicate VRAM allocations."""
|
|
24
|
+
import struct
|
|
25
|
+
|
|
26
|
+
param_dict = dict(model.named_parameters())
|
|
27
|
+
buffer_dict = dict(model.named_buffers())
|
|
28
|
+
|
|
29
|
+
with open(sf_path, "rb") as f:
|
|
30
|
+
header_size = struct.unpack("<Q", f.read(8))[0]
|
|
31
|
+
header = json.loads(f.read(header_size).decode("utf-8"))
|
|
32
|
+
data_base_offset = 8 + header_size
|
|
33
|
+
|
|
34
|
+
for name, info in header.items():
|
|
35
|
+
if name == "__metadata__":
|
|
36
|
+
continue
|
|
37
|
+
start, end = info["data_offsets"]
|
|
38
|
+
length = end - start
|
|
39
|
+
f.seek(data_base_offset + start)
|
|
40
|
+
raw = f.read(length)
|
|
41
|
+
|
|
42
|
+
src_dt = DT_MAP.get(info["dtype"], torch.float16)
|
|
43
|
+
t = torch.frombuffer(bytearray(raw), dtype=src_dt).reshape(info["shape"])
|
|
44
|
+
|
|
45
|
+
if name in param_dict:
|
|
46
|
+
param_dict[name].data.copy_(t.to(dtype=target_dtype))
|
|
47
|
+
elif name in buffer_dict:
|
|
48
|
+
buffer_dict[name].data.copy_(t.to(dtype=target_dtype))
|
|
49
|
+
del raw, t
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_model_from_safetensors(
|
|
53
|
+
model_dir_or_repo: str | Path,
|
|
54
|
+
device: str | None = None,
|
|
55
|
+
dtype: torch.dtype | None = None,
|
|
56
|
+
) -> tuple[Qwen2ForCausalLM, Qwen2Config]:
|
|
57
|
+
"""Load Qwen2.5 weights directly from safetensors with zero host memory duplication.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
model_dir_or_repo: Local folder path or HuggingFace repo ID (e.g. 'Qwen/Qwen2.5-0.5B-Instruct').
|
|
61
|
+
device: Target torch device ('cuda', 'cpu').
|
|
62
|
+
dtype: Target torch dtype (torch.bfloat16, torch.float16, torch.float32).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple of (model, config).
|
|
66
|
+
"""
|
|
67
|
+
if device is None:
|
|
68
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
69
|
+
if dtype is None:
|
|
70
|
+
if torch.cuda.is_available() and torch.cuda.is_bf16_supported():
|
|
71
|
+
dtype = torch.bfloat16
|
|
72
|
+
elif torch.cuda.is_available():
|
|
73
|
+
dtype = torch.float16
|
|
74
|
+
else:
|
|
75
|
+
dtype = torch.float32
|
|
76
|
+
|
|
77
|
+
path = Path(model_dir_or_repo)
|
|
78
|
+
|
|
79
|
+
# If it's a HuggingFace Hub repo id and not an existing local directory, download via huggingface_hub
|
|
80
|
+
if not path.exists():
|
|
81
|
+
from huggingface_hub import snapshot_download
|
|
82
|
+
|
|
83
|
+
print(f"Downloading model {model_dir_or_repo} from Hugging Face Hub...")
|
|
84
|
+
path = Path(snapshot_download(repo_id=str(model_dir_or_repo), allow_patterns=["*.safetensors", "config.json", "tokenizer*"]))
|
|
85
|
+
|
|
86
|
+
# 1. Load config.json
|
|
87
|
+
config_path = path / "config.json"
|
|
88
|
+
if not config_path.exists():
|
|
89
|
+
raise FileNotFoundError(f"config.json not found in {path}")
|
|
90
|
+
|
|
91
|
+
with open(config_path, encoding="utf-8") as f:
|
|
92
|
+
config_dict = json.load(f)
|
|
93
|
+
|
|
94
|
+
config = Qwen2Config.from_dict(config_dict)
|
|
95
|
+
|
|
96
|
+
# 2. Instantiate model directly with target dtype on target device
|
|
97
|
+
print(f"Initializing Qwen2ForCausalLM ({config.num_hidden_layers} layers, hidden_size={config.hidden_size})...")
|
|
98
|
+
orig_dtype = torch.get_default_dtype()
|
|
99
|
+
torch.set_default_dtype(dtype)
|
|
100
|
+
try:
|
|
101
|
+
model = Qwen2ForCausalLM(config).to(device=device, dtype=dtype)
|
|
102
|
+
finally:
|
|
103
|
+
torch.set_default_dtype(orig_dtype)
|
|
104
|
+
|
|
105
|
+
# 3. Locate safetensors files
|
|
106
|
+
safetensor_files = sorted(list(path.glob("*.safetensors")))
|
|
107
|
+
if not safetensor_files:
|
|
108
|
+
raise FileNotFoundError(f"No .safetensors files found in {path}")
|
|
109
|
+
|
|
110
|
+
print(f"Streaming weights from {len(safetensor_files)} safetensors file(s) into {device} ({dtype})...")
|
|
111
|
+
for sf in safetensor_files:
|
|
112
|
+
_load_safetensors_into_model(model, sf, target_dtype=dtype)
|
|
113
|
+
|
|
114
|
+
# 4. Handle tied word embeddings
|
|
115
|
+
if config.tie_word_embeddings:
|
|
116
|
+
model.lm_head.weight = model.model.embed_tokens.weight
|
|
117
|
+
|
|
118
|
+
if torch.cuda.is_available():
|
|
119
|
+
torch.cuda.empty_cache()
|
|
120
|
+
|
|
121
|
+
model.eval()
|
|
122
|
+
return model, config
|