Skip to content

Commit 7ce74d1

Browse files
committed
feat(opsd): share prompt prefill across rollout samples
Signed-off-by: nathon-lee <leejianwoo@gmail.com>
1 parent da3ca68 commit 7ce74d1

7 files changed

Lines changed: 364 additions & 17 deletions

File tree

csrc/transformer/inference/csrc/pt_binding.cpp

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,12 +478,13 @@ std::vector<at::Tensor> ds_softmax_context(at::Tensor& query_key_value,
478478
auto output = torch::from_blob(workspace + 4 * buf_size, {bsz, seq_len, hidden_dim}, options);
479479

480480
auto query_cont = workspace + 5 * buf_size;
481+
unsigned cache_bsz = InferenceContext::Instance().GetBatchSize();
481482
size_t offset =
482-
10 * (hidden_dim * bsz * InferenceContext::Instance().GetMaxTokenLength()) +
483-
layer_id * 2 * bsz * InferenceContext::Instance().GetMaxTokenLength() * hidden_dim;
483+
10 * (hidden_dim * cache_bsz * InferenceContext::Instance().GetMaxTokenLength()) +
484+
layer_id * 2 * cache_bsz * InferenceContext::Instance().GetMaxTokenLength() * hidden_dim;
484485
unsigned all_tokens = soft_len;
485486
auto kv_cache = workspace + offset + (hidden_dim / heads) * (is_prompt ? 0 : soft_len - 1);
486-
size_t value_offset = bsz * InferenceContext::Instance().GetMaxTokenLength() * hidden_dim;
487+
size_t value_offset = cache_bsz * InferenceContext::Instance().GetMaxTokenLength() * hidden_dim;
487488

488489
T* temp_buf = (T*)output.data_ptr() + at::numel(output);
489490
launch_bias_add_transform_0213<T>((T*)query_cont,
@@ -1943,6 +1944,83 @@ void ds_release_workspace() { InferenceContext::Instance().release_workspace();
19431944

19441945
bool ds_retake_workspace() { return InferenceContext::Instance().retake_workspace(); }
19451946

1947+
template <typename T>
1948+
at::ScalarType workspace_scalar_type();
1949+
1950+
template <>
1951+
at::ScalarType workspace_scalar_type<float>()
1952+
{
1953+
return torch::kFloat32;
1954+
}
1955+
1956+
template <>
1957+
at::ScalarType workspace_scalar_type<__half>()
1958+
{
1959+
return torch::kFloat16;
1960+
}
1961+
1962+
#ifdef BF16_AVAILABLE
1963+
template <>
1964+
at::ScalarType workspace_scalar_type<__nv_bfloat16>()
1965+
{
1966+
return torch::kBFloat16;
1967+
}
1968+
#endif
1969+
1970+
template <typename T>
1971+
std::vector<at::Tensor> repeat_kv_cache(unsigned source_batch_size, unsigned repeats)
1972+
{
1973+
auto& context = InferenceContext::Instance();
1974+
const auto target_batch_size = source_batch_size * repeats;
1975+
if (repeats < 1 || source_batch_size < 1 || target_batch_size != context.GetBatchSize()) {
1976+
throw std::runtime_error(
1977+
"KV cache repeat does not match the allocated workspace batch size");
1978+
}
1979+
1980+
const auto num_layers = context.GetNumLayers();
1981+
const auto num_heads = context.GetNumHeads();
1982+
const auto max_tokens = context.GetMaxTokenLength();
1983+
const auto hidden_dim = context.GetHiddenDim();
1984+
const auto head_dim = hidden_dim / num_heads;
1985+
const auto current_tokens = context.current_tokens();
1986+
if (current_tokens <= 1) {
1987+
throw std::runtime_error("KV cache repeat requires a completed prompt forward");
1988+
}
1989+
const auto prompt_tokens = current_tokens - 1;
1990+
auto options = at::TensorOptions()
1991+
.dtype(workspace_scalar_type<T>())
1992+
.layout(at::kStrided)
1993+
.device(at::kCUDA)
1994+
.requires_grad(false);
1995+
T* workspace = (T*)context.GetWorkSpace();
1996+
const auto cache_offset = 10 * hidden_dim * target_batch_size * max_tokens;
1997+
auto cache = torch::from_blob(workspace + cache_offset,
1998+
{(long)num_layers,
1999+
2,
2000+
(long)target_batch_size,
2001+
(long)num_heads,
2002+
(long)max_tokens,
2003+
(long)head_dim},
2004+
options);
2005+
// Backward copies preserve source rows that overlap the expanded destination range.
2006+
for (unsigned destination = target_batch_size; destination-- > 0;) {
2007+
const auto source = destination / repeats;
2008+
if (source == destination) { continue; }
2009+
auto destination_cache = cache.select(2, destination).slice(3, 0, prompt_tokens);
2010+
auto source_cache = cache.select(2, source).slice(3, 0, prompt_tokens);
2011+
destination_cache.copy_(source_cache);
2012+
}
2013+
2014+
std::vector<at::Tensor> repeated_cache;
2015+
repeated_cache.reserve(num_layers * 2);
2016+
for (unsigned layer = 0; layer < num_layers; layer++) {
2017+
auto layer_cache = cache.select(0, layer);
2018+
repeated_cache.push_back(layer_cache.select(0, 0).slice(2, 0, prompt_tokens));
2019+
repeated_cache.push_back(layer_cache.select(0, 1).slice(2, 0, prompt_tokens));
2020+
}
2021+
return repeated_cache;
2022+
}
2023+
19462024
template <typename T>
19472025
at::Tensor ds_dequantize(at::Tensor& weight, at::Tensor& qscale, int groups)
19482026
{
@@ -2032,6 +2110,9 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m)
20322110
m.def("allocate_workspace_" #_name, \
20332111
&allocate_workspace<_dtype>, \
20342112
"DeepSpeed memory allocation for GPT inference with " #_name " (CUDA)"); \
2113+
m.def("repeat_kv_cache_" #_name, \
2114+
&repeat_kv_cache<_dtype>, \
2115+
"Repeat prompt KV cache entries across the inference batch with " #_name " (CUDA)"); \
20352116
m.def("dequantize_" #_name, \
20362117
&ds_dequantize<_dtype>, \
20372118
"DeepSpeed dequantize with " #_name " (CUDA)");

csrc/transformer/inference/includes/inference_context.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ class InferenceContext {
6060
{
6161
_workSpaceSize = 0;
6262
_workspace = 0;
63+
_batch_size = 0;
64+
_num_layers = 0;
65+
_num_heads = 0;
66+
_hidden_dim = 0;
6367

6468
cublasStatus_t stat = cublasCreate(&_cublasHandle);
6569
if (stat != CUBLAS_STATUS_SUCCESS) {
@@ -108,6 +112,10 @@ class InferenceContext {
108112
unsigned min_out_tokens)
109113
{
110114
size_t total_size;
115+
_batch_size = batch_size;
116+
_num_layers = num_layers;
117+
_num_heads = num_heads;
118+
_hidden_dim = hidden_dim;
111119
if (!_free_memory_size) { cudaMemGetInfo(&_free_memory_size, &total_size); }
112120

113121
// Flash attention requires padded heads and we'll conservatively allocate
@@ -181,6 +189,10 @@ class InferenceContext {
181189
_attention_unfused_workspace_offset = workSpaceSize - temp_size;
182190
}
183191
inline size_t GetMaxTokenLength() const { return _max_seq_len; }
192+
inline size_t GetBatchSize() const { return _batch_size; }
193+
inline unsigned GetNumLayers() const { return _num_layers; }
194+
inline unsigned GetNumHeads() const { return _num_heads; }
195+
inline size_t GetHiddenDim() const { return _hidden_dim; }
184196

185197
cudaEvent_t GetCompEvent(int id) { return id == 1 ? _comp1_event : _comp2_event; }
186198

@@ -275,6 +287,10 @@ class InferenceContext {
275287
size_t _free_memory_size;
276288

277289
size_t _max_seq_len;
290+
size_t _batch_size;
291+
unsigned _num_layers;
292+
unsigned _num_heads;
293+
size_t _hidden_dim;
278294

279295
cudaEvent_t _comp1_event;
280296
cudaEvent_t _comp2_event;

deepspeed/ops/transformer/inference/op_binding/workspace.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,14 @@ def __init__(self, config: DeepSpeedInferenceConfig = None):
158158
super(WorkspaceOp, self).__init__(config)
159159
if config.dtype == torch.float32:
160160
self.allocate_workspace_func = self.inference_module.allocate_workspace_fp32
161+
repeat_kv_cache_name = "repeat_kv_cache_fp32"
161162
elif config.dtype == torch.bfloat16:
162163
self.allocate_workspace_func = self.inference_module.allocate_workspace_bf16
164+
repeat_kv_cache_name = "repeat_kv_cache_bf16"
163165
else:
164166
self.allocate_workspace_func = self.inference_module.allocate_workspace_fp16
167+
repeat_kv_cache_name = "repeat_kv_cache_fp16"
168+
self.repeat_kv_cache_func = getattr(self.inference_module, repeat_kv_cache_name, None)
165169
self.release_workspace_func = self.inference_module.release_workspace
166170
self.retake_workspace_func = self.inference_module.retake_workspace
167171
self.reset_cache_func = self.inference_module.reset_cache
@@ -176,6 +180,7 @@ def __init__(self, config: DeepSpeedInferenceConfig = None):
176180
self.release_workspace_func = self.release_workspace_fallback
177181
self.retake_workspace_func = self.retake_workspace_fallback
178182
self.reset_cache_func = self.reset_cache_fallback
183+
self.repeat_kv_cache_func = self.repeat_kv_cache_fallback
179184

180185
def allocate_workspace(self, *args, **kwargs):
181186
self._is_allocated = True
@@ -191,6 +196,11 @@ def reset_cache(self):
191196
def retake_workspace(self):
192197
return self.retake_workspace_func() if self.retake_workspace_func else None
193198

199+
def repeat_kv_cache(self, source_batch_size, repeats):
200+
if self.repeat_kv_cache_func is None:
201+
raise RuntimeError("Shared prefill requires rebuilding the transformer inference extension")
202+
return self.repeat_kv_cache_func(source_batch_size, repeats)
203+
194204
def allocate_workspace_fp32_fallback(self, hidden_dim, num_heads, prompt_length, batch_size, num_layers, mp_size,
195205
external_cache, rank, max_out_tokens, min_out_tokens):
196206
return self.inference_context.gen_workspace(num_layers, num_heads, batch_size, prompt_length, hidden_dim,
@@ -218,5 +228,28 @@ def release_workspace_fallback(self):
218228
def retake_workspace_fallback(self):
219229
return self.inference_context.retake_workspace()
220230

231+
def repeat_kv_cache_fallback(self, source_batch_size, repeats):
232+
target_batch_size = source_batch_size * repeats
233+
cache_size = self.inference_context.kv_cache_size
234+
if cache_size is None or cache_size[0] != target_batch_size:
235+
raise RuntimeError("KV cache repeat does not match the allocated workspace batch size")
236+
if self.inference_context.kv_cache is None:
237+
raise RuntimeError("KV cache repeat requires a completed prompt forward")
238+
current_tokens = self.inference_context.current_tokens()
239+
if current_tokens <= 1:
240+
raise RuntimeError("KV cache repeat requires a completed prompt forward")
241+
prompt_tokens = current_tokens - 1
242+
repeated_cache = []
243+
for key_cache, value_cache in self.inference_context.kv_cache:
244+
# Backward copies preserve source rows that overlap the expanded destination range.
245+
for destination in range(target_batch_size - 1, -1, -1):
246+
source = destination // repeats
247+
if source == destination:
248+
continue
249+
key_cache[destination, :, :prompt_tokens, :].copy_(key_cache[source, :, :prompt_tokens, :])
250+
value_cache[destination, :, :prompt_tokens, :].copy_(value_cache[source, :, :prompt_tokens, :])
251+
repeated_cache.extend((key_cache[:, :, :prompt_tokens, :], value_cache[:, :, :prompt_tokens, :]))
252+
return repeated_cache
253+
221254
def is_allocated(self):
222255
return self._is_allocated

deepspeed/runtime/hybrid_engine.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,46 @@ def retake_inference_cache(self):
177177
if not retake_success:
178178
raise RuntimeError("Unable to retake inference workspace.")
179179

180+
def prepare_shared_prefill(self, source_batch_size, repeats, prompt_length):
181+
"""Allocate a target-batch workspace before a shared prompt forward."""
182+
hybrid_config = self._config.hybrid_engine
183+
if self.Z3_enabled:
184+
raise RuntimeError("Shared prefill does not support ZeRO stage 3")
185+
if hybrid_config.inference_tp_size != 1:
186+
raise RuntimeError("Shared prefill does not support inference tensor parallelism")
187+
if hybrid_config.release_inference_cache:
188+
raise RuntimeError("Shared prefill does not support release_inference_cache")
189+
if hybrid_config.enable_cuda_graph:
190+
raise RuntimeError("Shared prefill does not support CUDA graph capture")
191+
if len(self._inference_containers) == 0:
192+
raise RuntimeError("Shared prefill requires HybridEngine inference containers")
193+
194+
target_batch_size = source_batch_size * repeats
195+
inference_module = self._inference_containers[0].module
196+
config = inference_module.config
197+
if config.bigscience_bloom:
198+
raise RuntimeError("Shared prefill does not support external KV caches")
199+
inference_module.workspace.allocate_workspace(
200+
config.hidden_size,
201+
config.heads,
202+
prompt_length,
203+
target_batch_size,
204+
len(self._inference_containers),
205+
config.mp_size,
206+
config.bigscience_bloom,
207+
dist.get_rank() if dist.is_initialized() else 0,
208+
config.max_out_tokens,
209+
config.min_out_tokens,
210+
)
211+
for container in self._inference_containers:
212+
container.module._should_allocate_workspace = False
213+
self._shared_prefill_workspace = inference_module.workspace
214+
215+
def repeat_shared_prefill_cache(self, source_batch_size, repeats):
216+
"""Expand the completed prompt cache for independent response branches."""
217+
cache_tensors = self._shared_prefill_workspace.repeat_kv_cache(source_batch_size, repeats)
218+
return tuple(zip(cache_tensors[::2], cache_tensors[1::2]))
219+
180220
def generate(self, *inputs, **kwargs):
181221
if self._total_batch_size is None:
182222
bsz = inputs[0].shape[0] if len(inputs) > 0 else \

deepspeed/runtime/rollout/hybrid_engine_rollout.py

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class HybridEngineRolloutConfig:
2626
"""Configuration for HybridEngineRollout."""
2727
use_graph_capture: bool = False
2828
enable_profiling: bool = False
29+
use_shared_prefill: bool = False
2930

3031

3132
class HybridEngineRollout(RolloutEngine):
@@ -42,6 +43,7 @@ def __init__(self, engine, tokenizer, cfg=None):
4243
self.tokenizer = tokenizer
4344
self.use_graph_capture = getattr(cfg, 'use_graph_capture', False) if cfg else False
4445
self.enable_profiling = getattr(cfg, 'enable_profiling', False) if cfg else False
46+
self.use_shared_prefill = getattr(cfg, 'use_shared_prefill', False) if cfg else False
4547
self._last_profile = None
4648

4749
@torch.no_grad()
@@ -77,20 +79,31 @@ def generate(self, request: RolloutRequest, sampling: SamplingConfig) -> Rollout
7779

7880
is_greedy = sampling.temperature <= 0.0
7981

80-
if self.use_graph_capture and is_greedy:
81-
output_ids = self._generate_graph(prompt_ids, prompt_attn, max_new_tokens, pad_token_id, module, device)
82-
else:
83-
temperature = max(sampling.temperature, 1e-8)
84-
do_sample = not is_greedy
85-
output_ids = module.generate(
86-
prompt_ids,
87-
attention_mask=prompt_attn,
88-
max_new_tokens=max_new_tokens,
89-
do_sample=do_sample,
90-
temperature=temperature if do_sample else 1.0,
91-
top_p=sampling.top_p if do_sample else 1.0,
92-
pad_token_id=pad_token_id,
93-
)
82+
shared_prefill_handles = []
83+
if self.use_shared_prefill and n > 1:
84+
if self.use_graph_capture:
85+
raise RuntimeError("Shared prefill does not support CUDA graph capture")
86+
self.engine.prepare_shared_prefill(B, n, prompt_len)
87+
shared_prefill_handles = self._register_shared_prefill_hooks(module, B, n)
88+
try:
89+
if self.use_graph_capture and is_greedy:
90+
output_ids = self._generate_graph(prompt_ids, prompt_attn, max_new_tokens, pad_token_id, module,
91+
device)
92+
else:
93+
temperature = max(sampling.temperature, 1e-8)
94+
do_sample = not is_greedy
95+
output_ids = module.generate(
96+
prompt_ids,
97+
attention_mask=prompt_attn,
98+
max_new_tokens=max_new_tokens,
99+
do_sample=do_sample,
100+
temperature=temperature if do_sample else 1.0,
101+
top_p=sampling.top_p if do_sample else 1.0,
102+
pad_token_id=pad_token_id,
103+
)
104+
finally:
105+
for handle in shared_prefill_handles:
106+
handle.remove()
94107

95108
if self.enable_profiling:
96109
accelerator.synchronize()
@@ -141,6 +154,43 @@ def get_last_profile(self):
141154
"""Return the most recent profiling snapshot for this rollout instance."""
142155
return self._last_profile
143156

157+
def _register_shared_prefill_hooks(self, module, batch_size, repeats):
158+
state = {"pending": True, "reduced": False}
159+
160+
def reduce_prompt_batch(_module, args, kwargs):
161+
input_ids = kwargs.get("input_ids")
162+
if not state["pending"]:
163+
return args, kwargs
164+
if input_ids is None:
165+
raise RuntimeError("Shared prefill requires input_ids as a keyword argument")
166+
expected_batch_size = batch_size * repeats
167+
if input_ids.shape[0] != expected_batch_size:
168+
raise RuntimeError("Shared prefill input batch does not match the expanded rollout batch")
169+
if input_ids.shape[1] <= 1:
170+
raise RuntimeError("Shared prefill requires a prompt with more than one token")
171+
kwargs = dict(kwargs)
172+
kwargs["input_ids"] = input_ids[::repeats]
173+
for name in ("attention_mask", "position_ids", "token_type_ids"):
174+
value = kwargs.get(name)
175+
if isinstance(value, torch.Tensor) and value.shape[0] == expected_batch_size:
176+
kwargs[name] = value[::repeats]
177+
state["reduced"] = True
178+
return args, kwargs
179+
180+
def expand_prompt_output(_module, _args, _kwargs, output):
181+
if not state["pending"]:
182+
return output
183+
if not state["reduced"]:
184+
raise RuntimeError("Shared prefill did not reduce the prompt batch")
185+
state["pending"] = False
186+
output.past_key_values = self.engine.repeat_shared_prefill_cache(batch_size, repeats)
187+
output.logits = output.logits.repeat_interleave(repeats, dim=0)
188+
return output
189+
190+
pre_handle = module.register_forward_pre_hook(reduce_prompt_batch, with_kwargs=True)
191+
post_handle = module.register_forward_hook(expand_prompt_output, with_kwargs=True)
192+
return pre_handle, post_handle
193+
144194
# ------------------------------------------------------------------
145195
# Graph capture decode loop (greedy only)
146196
# ------------------------------------------------------------------

docs/code-docs/source/inference-engine.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,17 @@ batch size, samples per prompt, prompt length, and returned response length.
4343
For benchmark matrices, cases execute from the largest effective batch to the
4444
smallest because HybridEngine sizes its inference workspace on the first
4545
forward. Results remain in the user-requested matrix order.
46+
47+
Shared Prompt Prefill
48+
---------------------
49+
50+
When one prompt branches into multiple response samples,
51+
``HybridEngineRolloutConfig(use_shared_prefill=True)`` computes the prompt
52+
forward once and repeats its KV cache before decoding the independent response
53+
branches. The option is disabled by default.
54+
55+
Shared prefill currently requires HybridEngine kernel injection, ZeRO stage 0,
56+
inference tensor-parallel size 1, an internal KV cache, and a prompt longer than
57+
one token. It cannot be combined with CUDA graph capture or
58+
``release_inference_cache``. Sampling still happens independently for every
59+
response branch after the shared prompt forward.

0 commit comments

Comments
 (0)