forked from deepspeedai/DeepSpeed
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhybrid_engine_rollout.py
More file actions
341 lines (296 loc) · 14.4 KB
/
Copy pathhybrid_engine_rollout.py
File metadata and controls
341 lines (296 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
"""Rollout engine backed by DeepSpeed's hybrid engine.
Two generation paths:
1. **model.generate()** (default): delegates to HuggingFace generate.
Supports sampling (temperature, top_p) and greedy.
2. **graph capture + DeepSpeedStaticCache**: only for greedy (temperature=0).
Pre-allocates a StaticCache, captures the decode forward pass with a
CUDA graph, and replays it for each decode step. Eliminates kernel
launch overhead.
"""
import time
from dataclasses import dataclass
import torch
from deepspeed.accelerator import get_accelerator
from deepspeed.runtime.rollout.base import RolloutBatch, RolloutEngine, RolloutRequest, SamplingConfig
@dataclass
class HybridEngineRolloutConfig:
"""Configuration for HybridEngineRollout."""
use_graph_capture: bool = False
enable_profiling: bool = False
use_shared_prefill: bool = False
class HybridEngineRollout(RolloutEngine):
"""Rollout engine using DeepSpeed hybrid engine.
Args:
engine: DeepSpeed engine wrapping the model.
tokenizer: HuggingFace tokenizer (must have pad_token_id or eos_token_id).
cfg: Optional HybridEngineRolloutConfig.
"""
def __init__(self, engine, tokenizer, cfg=None):
self.engine = engine
self.tokenizer = tokenizer
self.use_graph_capture = getattr(cfg, 'use_graph_capture', False) if cfg else False
self.enable_profiling = getattr(cfg, 'enable_profiling', False) if cfg else False
self.use_shared_prefill = getattr(cfg, 'use_shared_prefill', False) if cfg else False
self._last_profile = None
@torch.no_grad()
def generate(self, request: RolloutRequest, sampling: SamplingConfig) -> RolloutBatch:
device = request.prompt_ids.device
B = request.prompt_ids.shape[0]
n = sampling.n_samples_per_prompt
total = B * n
prompt_len = request.prompt_ids.shape[1]
max_new_tokens = sampling.max_new_tokens
pad_token_id = self.tokenizer.pad_token_id
if pad_token_id is None:
pad_token_id = self.tokenizer.eos_token_id
module = self.engine.module
if self.enable_profiling:
accelerator = get_accelerator()
accelerator.synchronize()
profile_start = time.perf_counter()
# Expand prompts for n samples per prompt
if n > 1:
prompt_ids = request.prompt_ids.repeat_interleave(n, dim=0)
prompt_attn = request.prompt_attention_mask.repeat_interleave(n, dim=0)
else:
prompt_ids = request.prompt_ids
prompt_attn = request.prompt_attention_mask
if self.enable_profiling:
accelerator.synchronize()
expansion_end = time.perf_counter()
is_greedy = sampling.temperature <= 0.0
shared_prefill_handles = []
if self.use_shared_prefill and n > 1:
if self.use_graph_capture:
raise RuntimeError("Shared prefill does not support CUDA graph capture")
self.engine.prepare_shared_prefill(B, n, prompt_len)
shared_prefill_handles = self._register_shared_prefill_hooks(module, B, n)
try:
if self.use_graph_capture and is_greedy:
output_ids = self._generate_graph(prompt_ids, prompt_attn, max_new_tokens, pad_token_id, module,
device)
else:
temperature = max(sampling.temperature, 1e-8)
do_sample = not is_greedy
output_ids = module.generate(
prompt_ids,
attention_mask=prompt_attn,
max_new_tokens=max_new_tokens,
do_sample=do_sample,
temperature=temperature if do_sample else 1.0,
top_p=sampling.top_p if do_sample else 1.0,
pad_token_id=pad_token_id,
)
finally:
for handle in shared_prefill_handles:
handle.remove()
if self.enable_profiling:
accelerator.synchronize()
generation_end = time.perf_counter()
# Build attention mask: pad positions (both left padding from prompt
# and right padding from EOS / shorter sequences) are 0.
response_start = prompt_len
attention_mask = (output_ids != pad_token_id).long()
for i in range(total):
prompt_valid = request.prompt_attention_mask[i // n if B > 1 else 0]
attention_mask[i, :prompt_len] = prompt_valid
rollout_batch = RolloutBatch(
input_ids=output_ids,
attention_mask=attention_mask,
response_start_idx=torch.full((total, ), response_start, dtype=torch.long, device=device),
)
if self.enable_profiling:
accelerator.synchronize()
post_processing_end = time.perf_counter()
prompt_expansion_ms = (expansion_end - profile_start) * 1000.0
generation_ms = (generation_end - expansion_end) * 1000.0
post_processing_ms = (post_processing_end - generation_end) * 1000.0
total_ms = (post_processing_end - profile_start) * 1000.0
response_length = int(output_ids.shape[1] - prompt_len)
num_generated_tokens = int(output_ids.shape[0] * response_length)
tokens_per_second = 0.0
if total_ms > 0.0:
tokens_per_second = num_generated_tokens / (total_ms / 1000.0)
self._last_profile = {
"prompt_expansion_ms": prompt_expansion_ms,
"generation_ms": generation_ms,
"post_processing_ms": post_processing_ms,
"total_ms": total_ms,
"num_generated_tokens": num_generated_tokens,
"tokens_per_second": tokens_per_second,
"batch_size": B,
"num_samples_per_prompt": n,
"prompt_length": prompt_len,
"response_length": response_length,
}
return rollout_batch
def get_last_profile(self):
"""Return the most recent profiling snapshot for this rollout instance."""
return self._last_profile
def _register_shared_prefill_hooks(self, module, batch_size, repeats):
state = {"pending": True, "reduced": False}
def reduce_prompt_batch(_module, args, kwargs):
input_ids = kwargs.get("input_ids")
if not state["pending"]:
return args, kwargs
if input_ids is None:
raise RuntimeError("Shared prefill requires input_ids as a keyword argument")
expected_batch_size = batch_size * repeats
if input_ids.shape[0] != expected_batch_size:
raise RuntimeError("Shared prefill input batch does not match the expanded rollout batch")
if input_ids.shape[1] <= 1:
raise RuntimeError("Shared prefill requires a prompt with more than one token")
kwargs = dict(kwargs)
kwargs["input_ids"] = input_ids[::repeats]
for name in ("attention_mask", "position_ids", "token_type_ids"):
value = kwargs.get(name)
if isinstance(value, torch.Tensor) and value.shape[0] == expected_batch_size:
kwargs[name] = value[::repeats]
state["reduced"] = True
return args, kwargs
def expand_prompt_output(_module, _args, _kwargs, output):
if not state["pending"]:
return output
if not state["reduced"]:
raise RuntimeError("Shared prefill did not reduce the prompt batch")
state["pending"] = False
output.past_key_values = self.engine.repeat_shared_prefill_cache(batch_size, repeats)
output.logits = output.logits.repeat_interleave(repeats, dim=0)
return output
pre_handle = module.register_forward_pre_hook(reduce_prompt_batch, with_kwargs=True)
post_handle = module.register_forward_hook(expand_prompt_output, with_kwargs=True)
return pre_handle, post_handle
# ------------------------------------------------------------------
# Graph capture decode loop (greedy only)
# ------------------------------------------------------------------
def _generate_graph(self, prompt_ids, prompt_attn, max_new_tokens, pad_token_id, module, device):
"""Greedy decode with DeepSpeedStaticCache + CUDA graph capture."""
from transformers import StaticCache
from deepspeed.utils.static_cache import DeepSpeedStaticCache
batch_size = prompt_ids.shape[0]
prompt_len = prompt_ids.shape[1]
max_len = prompt_len + max_new_tokens
eos_token_id = self.tokenizer.eos_token_id
model_dtype = next(module.parameters()).dtype
# --- Prefill with HF StaticCache (correct attention semantics) ---
prefill_cache = StaticCache(
config=module.config,
batch_size=batch_size,
max_cache_len=max_len,
device=device,
dtype=model_dtype,
)
prefill_attn = torch.ones(batch_size, prompt_len, dtype=torch.long, device=device)
prefill_attn[:, :prompt_len] = prompt_attn
prefill_out = module(
prompt_ids,
attention_mask=prefill_attn,
past_key_values=prefill_cache,
use_cache=True,
cache_position=torch.arange(prompt_len, device=device),
)
next_token = prefill_out.logits[:, -1, :].argmax(dim=-1, keepdim=True)
# --- Copy prefill KV into DeepSpeedStaticCache ---
write_pos = torch.tensor(prompt_len - 1, dtype=torch.long, device=device)
ds_cache = DeepSpeedStaticCache(
module.config,
batch_size=batch_size,
max_cache_len=max_len,
device=device,
dtype=model_dtype,
)
ds_cache.set_write_position(write_pos)
# Trigger lazy init then copy real data
for layer_idx in range(len(ds_cache.layers)):
ds_layer = ds_cache.layers[layer_idx]
hf_layer = prefill_cache.layers[layer_idx]
if not ds_layer.is_initialized:
ds_layer.lazy_initialization(hf_layer.keys, hf_layer.values)
ds_layer.keys[:, :, :prompt_len, :].copy_(hf_layer.keys[:, :, :prompt_len, :])
ds_layer.values[:, :, :prompt_len, :].copy_(hf_layer.values[:, :, :prompt_len, :])
output_ids = [prompt_ids, next_token]
# --- Static buffers for graph capture ---
static_token = torch.zeros(batch_size, 1, dtype=torch.long, device=device)
static_attn = torch.zeros(batch_size, max_len, dtype=torch.long, device=device)
static_attn[:, :prompt_len] = prompt_attn
static_attn[:, prompt_len] = 1 # first decode position
static_pos = torch.tensor(prompt_len, dtype=torch.long, device=device)
static_cache_pos = static_pos.unsqueeze(0) # [1] for cache_position
static_pos_ids = static_pos.reshape(1, 1).expand(batch_size, 1) # [batch, 1]
write_pos.fill_(prompt_len)
# Remove forward hooks (they synchronize — illegal during graph capture)
saved_pre = dict(module._forward_pre_hooks)
saved_post = dict(module._forward_hooks)
module._forward_pre_hooks.clear()
module._forward_hooks.clear()
try:
# Warmup on side stream
static_token.copy_(next_token)
s = get_accelerator().Stream()
s.wait_stream(get_accelerator().current_stream())
with get_accelerator().stream(s):
for _ in range(3):
out = module(
static_token,
attention_mask=static_attn,
past_key_values=ds_cache,
use_cache=True,
cache_position=static_cache_pos,
position_ids=static_pos_ids,
)
get_accelerator().current_stream().wait_stream(s)
# Capture
graph = get_accelerator().create_graph()
with get_accelerator().capture_to_graph(graph):
out = module(
static_token,
attention_mask=static_attn,
past_key_values=ds_cache,
use_cache=True,
cache_position=static_cache_pos,
position_ids=static_pos_ids,
)
static_logits = out.logits
finally:
module._forward_pre_hooks.update(saved_pre)
module._forward_hooks.update(saved_post)
# --- Decode loop ---
eos_mask = torch.zeros(batch_size, dtype=torch.bool, device=device)
for step in range(max_new_tokens - 1):
if eos_mask.all():
output_ids.append(torch.full((batch_size, 1), pad_token_id, dtype=torch.long, device=device))
continue
# Update static inputs
static_token.copy_(next_token)
pos = prompt_len + step
write_pos.fill_(pos)
static_cache_pos.fill_(pos)
static_pos_ids.fill_(pos)
static_attn[:, pos] = 1
# Replay
get_accelerator().replay_graph(graph)
next_token = static_logits[:, -1, :].argmax(dim=-1, keepdim=True)
output_ids.append(next_token)
eos_mask |= (next_token.squeeze(1) == eos_token_id)
return torch.cat(output_ids, dim=1)
@staticmethod
def _sample_top_p(logits: torch.Tensor, temperature: float = 1.0, top_p: float = 1.0) -> torch.Tensor:
"""Sample from logits with temperature and nucleus (top-p) filtering."""
logits = logits / temperature
if top_p < 1.0:
sorted_logits, sorted_indices = torch.sort(logits, descending=True, dim=-1)
cumulative_probs = torch.cumsum(torch.softmax(sorted_logits, dim=-1), dim=-1)
mask = (cumulative_probs - torch.softmax(sorted_logits, dim=-1)) >= top_p
sorted_logits[mask] = -float('inf')
probs = torch.softmax(sorted_logits, dim=-1)
sampled = torch.multinomial(probs, 1)
tokens = sorted_indices.gather(1, sampled)
else:
probs = torch.softmax(logits, dim=-1)
tokens = torch.multinomial(probs, 1)
return tokens
def sync_weights(self, step: int) -> None: # noqa: ARG002
"""No-op: hybrid engine reads model weights live."""
return None