Skip to content

Commit 1f95164

Browse files
authored
Merge branch 'master' into comms_logger
2 parents b12c086 + 80c8e5b commit 1f95164

7 files changed

Lines changed: 526 additions & 7 deletions

File tree

deepspeed/module_inject/layers.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,18 @@ def config_tp_params(self, weight):
381381
setattr(weight, DS_TENSOR_MODEL_PARALLEL, True)
382382
setattr(weight, DS_IS_REPLACED_MODULE, True)
383383

384+
@staticmethod
385+
def _shape_before_zero3_partition(param):
386+
"""Shape of ``param`` as it was before ZeRO-3 partitioned it.
387+
388+
ZeRO-3 replaces a partitioned parameter's local data with an empty 1-D
389+
tensor, so ``param.shape`` no longer describes the layer and indexing it
390+
raises. ZeRO-3 records the pre-partition shape as ``ds_shape``, which is
391+
what the universal-checkpoint metadata needs.
392+
"""
393+
ds_shape = getattr(param, 'ds_shape', None)
394+
return tuple(param.shape) if ds_shape is None else tuple(ds_shape)
395+
384396
def _set_param_uc_meta(self,
385397
param,
386398
*,
@@ -704,20 +716,22 @@ def uneven_partition(self, params_list):
704716
params_list[idx].data = _partition
705717

706718
def _mark_uc_metadata(self):
707-
original_weight_shape = (self.weight.shape[0], self.weight.shape[1] * self.tp_world_size)
719+
weight_shape = self._shape_before_zero3_partition(self.weight)
720+
original_weight_shape = (weight_shape[0], weight_shape[1] * self.tp_world_size)
708721
self._set_param_uc_meta(self.weight,
709722
partition_type='row',
710723
partition_dim=1,
711724
logical_shape=original_weight_shape,
712725
output_shape=(original_weight_shape[0], ),
713726
original_shape=original_weight_shape)
714727
if self.bias is not None:
728+
bias_shape = self._shape_before_zero3_partition(self.bias)
715729
self._set_param_uc_meta(self.bias,
716730
partition_type='row',
717731
partition_dim=None,
718-
logical_shape=tuple(self.bias.shape),
719-
output_shape=tuple(self.bias.shape),
720-
original_shape=tuple(self.bias.shape),
732+
logical_shape=bias_shape,
733+
output_shape=bias_shape,
734+
original_shape=bias_shape,
721735
is_bias=True,
722736
replicated=True)
723737

@@ -803,16 +817,17 @@ def uneven_partition(self, params_list):
803817
params_list[idx].data = _partition
804818

805819
def _mark_uc_metadata(self):
806-
original_out_dim = self.weight.shape[0] * self.tp_world_size
807-
original_weight_shape = (original_out_dim, self.weight.shape[1])
820+
weight_shape = self._shape_before_zero3_partition(self.weight)
821+
original_out_dim = weight_shape[0] * self.tp_world_size
822+
original_weight_shape = (original_out_dim, weight_shape[1])
808823
self._set_param_uc_meta(self.weight,
809824
partition_type='column',
810825
partition_dim=0,
811826
logical_shape=original_weight_shape,
812827
output_shape=(original_out_dim, ),
813828
original_shape=original_weight_shape)
814829
if self.bias is not None:
815-
original_bias_shape = (self.bias.shape[0] * self.tp_world_size, )
830+
original_bias_shape = (self._shape_before_zero3_partition(self.bias)[0] * self.tp_world_size, )
816831
self._set_param_uc_meta(self.bias,
817832
partition_type='column',
818833
partition_dim=0,

deepspeed/runtime/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,7 @@ class HybridEngineConfig(DeepSpeedConfigModel):
519519
release_inference_cache: bool = False
520520
pin_parameters: bool = True
521521
tp_gather_partition_size: int = 8
522+
enable_cuda_graph: bool = False
522523

523524

524525
def get_hybrid_engine_config(param_dict):

deepspeed/runtime/hybrid_engine.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from deepspeed.utils import logger
2020
from deepspeed.module_inject.layers import LinearLayer, Normalize, EmbeddingLayer, OPTEmbedding
2121
from ..ops.transformer.inference.op_binding.workspace import WorkspaceOp
22+
from .hybrid_engine_graph import (DecodeGraphCache, decode_steps_from_generate_kwargs, validate_cuda_graph_support)
2223

2324
try:
2425
import transformers
@@ -62,6 +63,17 @@ def __init__(self, args, model, **kwargs):
6263
self.is_lora_fused = False
6364
self.workspace = WorkspaceOp()
6465

66+
self._orig_module_forward = None
67+
self._decode_graphs = None
68+
if self._config.hybrid_engine.enable_cuda_graph and len(self._inference_containers) > 0:
69+
unsupported = validate_cuda_graph_support(self._config.hybrid_engine, self._config.zero_config.stage)
70+
if unsupported is not None:
71+
logger.warning(f"HybridEngine: running without CUDA graphs. {unsupported}.")
72+
else:
73+
self._orig_module_forward = self.module.forward
74+
self._decode_graphs = DecodeGraphCache(self._orig_module_forward,
75+
max_positions=self._config.hybrid_engine.max_out_tokens)
76+
6577
def convert_to_linear_transposed(self, model):
6678

6779
def _replace_linear_layer(r_module, parent_type=None, prev_type=None):
@@ -173,6 +185,9 @@ def generate(self, *inputs, **kwargs):
173185

174186
self._t0 = time.time()
175187

188+
if self._decode_graphs is not None:
189+
self._decode_graphs.begin_sequence(decode_steps_from_generate_kwargs(kwargs))
190+
176191
if self.Z3_enabled and self.gather_all_layers:
177192
if self._config.hybrid_engine.inference_tp_size > 1:
178193
non_tp_params = []
@@ -414,6 +429,8 @@ def eval(self):
414429
if not self.Z3_enabled or self.gather_all_layers:
415430
for orig_module, inference_layer in zip(self._orig_modules_others, self._other_layers):
416431
orig_module.forward = inference_layer.forward
432+
if self._decode_graphs is not None:
433+
self.module.forward = self._decode_graphs
417434
if self.Z3_enabled:
418435
gc.collect()
419436
get_accelerator().empty_cache()
@@ -428,6 +445,8 @@ def train(self, mode=True):
428445
orig_module.forward = orig_fwd
429446
for orig_module, orig_fwd in zip(self._orig_modules_others, self._orig_fwds_others):
430447
orig_module.forward = orig_fwd
448+
if self._decode_graphs is not None:
449+
self.module.forward = self._orig_module_forward
431450
super().train(mode)
432451
if mode:
433452
self._training_start_time = time.time()
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# DeepSpeed Team
3+
"""CUDA graph capture for HybridEngine token generation.
4+
5+
Generation in the HybridEngine is bound by CPU work rather than by the GPU: a
6+
single decode step issues on the order of a thousand kernel launches, and the
7+
kernels finish well before the host can queue the next step. Capturing the decode
8+
forward into a CUDA graph replaces all of those launches with one replay.
9+
10+
Two properties of the inference kernels shape this design.
11+
12+
First, a single graph cannot serve every decode position. The kernels read the
13+
current sequence length from a host-side counter
14+
(``InferenceContext::current_tokens()``) and pass it to kernels as a launch
15+
parameter. Capture freezes launch parameters, so a graph captured at one position
16+
would keep reading and writing that same position. Host code *does* run during
17+
capture, so capturing one graph per position records the correct offsets into
18+
each.
19+
20+
Second, that counter is advanced from host code and is not reachable from Python.
21+
Replay runs no host code, so it leaves the counter behind. An eager decode step
22+
after a replay would therefore use a stale sequence length and silently corrupt
23+
the KV cache. Eager and replayed decode steps must never be mixed within one
24+
sequence, which is why the decision is made once per sequence, up front, in
25+
``begin_sequence``.
26+
"""
27+
28+
import torch
29+
30+
from deepspeed.accelerator import get_accelerator
31+
from deepspeed.utils import logger
32+
33+
34+
def _tensor_kwargs(kwargs):
35+
"""Names of the keyword arguments that hold a tensor."""
36+
return [name for name, value in kwargs.items() if torch.is_tensor(value)]
37+
38+
39+
class DecodeGraphCache:
40+
"""Dispatches generation forwards to a per-position CUDA graph.
41+
42+
Args:
43+
forward: The original ``forward``, used both for capture and as the
44+
eager fallback.
45+
max_positions: Upper bound on how many decode positions may be captured,
46+
which bounds the memory the cache can consume.
47+
"""
48+
49+
def __init__(self, forward, max_positions):
50+
self._forward = forward
51+
self._max_positions = max_positions
52+
53+
self._graphs = {}
54+
self._static_kwargs = {}
55+
self._static_outputs = {}
56+
self._pool = None
57+
58+
self._position = 0
59+
self._captured_length = None
60+
self._use_graphs = False
61+
self._disabled = False
62+
63+
@property
64+
def captured_positions(self):
65+
return len(self._graphs)
66+
67+
def invalidate(self):
68+
"""Drop every captured graph and the memory pool backing them."""
69+
self._graphs.clear()
70+
self._static_kwargs.clear()
71+
self._static_outputs.clear()
72+
self._pool = None
73+
self._captured_length = None
74+
75+
def begin_sequence(self, num_decode_steps):
76+
"""Decide once, before any decode step, whether this sequence uses graphs.
77+
78+
Args:
79+
num_decode_steps: How many single-token forwards this generate call
80+
will make, or ``None`` when that is not known ahead of time.
81+
82+
A sequence runs entirely on graphs or entirely eagerly. Deciding here
83+
rather than per step is what keeps the host-side sequence counter
84+
consistent: a capture pass advances it on every step, and a replay pass
85+
never reads it.
86+
"""
87+
self._position = 0
88+
89+
if self._disabled or num_decode_steps is None or num_decode_steps <= 0:
90+
self._use_graphs = False
91+
return
92+
93+
if num_decode_steps > self._max_positions:
94+
self._use_graphs = False
95+
return
96+
97+
# A different generation length needs its own set of graphs, because each
98+
# graph has both its sequence offset and its attention-mask width baked in.
99+
if self._captured_length is not None and self._captured_length != num_decode_steps:
100+
logger.info(f"HybridEngine CUDA graph: generation length changed "
101+
f"{self._captured_length} -> {num_decode_steps}, recapturing.")
102+
self.invalidate()
103+
104+
self._captured_length = num_decode_steps
105+
self._use_graphs = True
106+
107+
def _capture(self, position, args, kwargs):
108+
"""Record a graph for ``position`` and keep its static input buffers."""
109+
static = {name: kwargs[name].clone() for name in _tensor_kwargs(kwargs)}
110+
capture_kwargs = dict(kwargs)
111+
capture_kwargs.update(static)
112+
113+
graph = get_accelerator().create_graph()
114+
if self._pool is None:
115+
with get_accelerator().capture_to_graph(graph):
116+
output = self._forward(*args, **capture_kwargs)
117+
self._pool = graph.pool()
118+
else:
119+
with get_accelerator().capture_to_graph(graph, pool=self._pool):
120+
output = self._forward(*args, **capture_kwargs)
121+
122+
self._graphs[position] = graph
123+
self._static_kwargs[position] = static
124+
self._static_outputs[position] = output
125+
126+
def _replay(self, position, kwargs):
127+
for name, buffer in self._static_kwargs[position].items():
128+
buffer.copy_(kwargs[name])
129+
get_accelerator().replay_graph(self._graphs[position])
130+
return self._static_outputs[position]
131+
132+
def _shapes_match(self, position, kwargs):
133+
captured = self._static_kwargs[position]
134+
if set(captured) != set(_tensor_kwargs(kwargs)):
135+
return False
136+
return all(captured[name].shape == kwargs[name].shape for name in captured)
137+
138+
def _fall_back(self, reason, args, kwargs):
139+
"""Give up on graphs for good and run eagerly.
140+
141+
Only safe before any replay has happened in the current sequence, which
142+
is why this is reachable only from the capture path.
143+
"""
144+
logger.warning(f"HybridEngine CUDA graph disabled: {reason}")
145+
self.invalidate()
146+
self._disabled = True
147+
self._use_graphs = False
148+
return self._forward(*args, **kwargs)
149+
150+
def __call__(self, *args, **kwargs):
151+
input_ids = kwargs.get("input_ids")
152+
is_decode_step = (input_ids is not None and input_ids.dim() == 2 and input_ids.shape[1] == 1)
153+
154+
if not self._use_graphs or not is_decode_step:
155+
return self._forward(*args, **kwargs)
156+
157+
position = self._position
158+
self._position += 1
159+
160+
if position >= self._max_positions:
161+
# begin_sequence() bounds the length, so this means the caller ran
162+
# longer than it declared. Replays have already happened, so eager
163+
# execution would read a stale sequence counter.
164+
raise RuntimeError(f"HybridEngine CUDA graph: generation exceeded the declared length "
165+
f"({self._max_positions} decode steps). Set hybrid_engine.max_out_tokens "
166+
f"to cover the longest generation, or disable hybrid_engine.enable_cuda_graph.")
167+
168+
if position in self._graphs:
169+
if self._shapes_match(position, kwargs):
170+
return self._replay(position, kwargs)
171+
return self._fall_back("input shapes changed mid-sequence", args, kwargs)
172+
173+
try:
174+
self._capture(position, args, kwargs)
175+
except Exception as err:
176+
return self._fall_back(f"capture failed at position {position}: {err}", args, kwargs)
177+
178+
# Capture records the work without running it, so the first execution must
179+
# come from a replay. That is also what fills the KV cache for this position.
180+
return self._replay(position, kwargs)
181+
182+
183+
def decode_steps_from_generate_kwargs(kwargs):
184+
"""How many single-token forwards a generate call will make, if knowable.
185+
186+
HF emits one prompt forward that produces the first new token, then one
187+
forward per remaining token. Only a pinned length is usable here: with an
188+
open-ended limit the sequence may stop early, and a sequence that runs
189+
*longer* than its captured graphs cannot fall back safely.
190+
"""
191+
max_new = kwargs.get("max_new_tokens")
192+
min_new = kwargs.get("min_new_tokens")
193+
if max_new is None or min_new is None or max_new != min_new:
194+
return None
195+
return int(max_new) - 1
196+
197+
198+
def validate_cuda_graph_support(config, zero_stage):
199+
"""Return the reason CUDA graphs cannot be used, or ``None`` if they can."""
200+
if not get_accelerator().is_available() or get_accelerator().device_name() != "cuda":
201+
return "CUDA graphs require the CUDA accelerator"
202+
203+
if zero_stage == 3:
204+
# Under ZeRO-3 the inference containers hold no persistent weights; the
205+
# parameters are gathered into fresh buffers for each generate call. A
206+
# graph would keep replaying whichever buffers existed at capture time,
207+
# which is silently wrong rather than merely slow.
208+
return "CUDA graphs are not supported with ZeRO stage 3"
209+
210+
if config.release_inference_cache:
211+
# Releasing the workspace frees the buffers the graphs write into.
212+
return "CUDA graphs are not supported with release_inference_cache"
213+
214+
if config.inference_tp_size > 1:
215+
return "CUDA graphs are not supported with inference_tp_size > 1"
216+
217+
return None

docs/_pages/config-json.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,36 @@ When a HuggingFace model provides a built-in `tp_plan` (via `model.config.base_m
876876
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
877877
| Unused parameters in modules may be unexpected in static networks, but could be normal in dynamic networks. This controls whether or not training should terminate with an error message when unused parameters are detected. This is set to `True` by default, which means unused parameters are ignored and training continues. Now is just used in stage 2. | `True` |
878878

879+
### Hybrid Engine
880+
881+
The Hybrid Engine (`DeepSpeedHybridEngine`) switches a model between training mode and DeepSpeed's inference kernels within a single training loop, which is what RLHF pipelines such as DeepSpeed-Chat use for the actor model.
882+
883+
```json
884+
"hybrid_engine": {
885+
"enabled": true,
886+
"max_out_tokens": 512,
887+
"inference_tp_size": 1,
888+
"release_inference_cache": false,
889+
"pin_parameters": true,
890+
"tp_gather_partition_size": 8,
891+
"enable_cuda_graph": false
892+
}
893+
```
894+
895+
***enable_cuda_graph***: [boolean]
896+
897+
| Description | Default |
898+
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
899+
| Capture token generation into CUDA graphs. Generation issues on the order of a thousand kernel launches per token and is bound by CPU launch overhead rather than by the GPU, so replaying a captured graph removes most of the per-token cost. One graph is captured per decode position, and generated tokens are unchanged. | `false` |
900+
901+
`enable_cuda_graph` requires a pinned generation length (`min_new_tokens` equal to `max_new_tokens`) and `max_out_tokens` large enough to cover the longest generation. It is ignored, with a warning, when any of the following apply, since captured graphs would not stay valid:
902+
903+
* ZeRO stage 3, where parameters are gathered into fresh buffers for each generation
904+
* `release_inference_cache: true`, which frees the buffers the graphs write into
905+
* `inference_tp_size` greater than 1
906+
907+
The first generation after enabling captures one graph per decode position and is therefore slower; subsequent generations replay them.
908+
879909
### Expert Parallel (AutoEP)
880910
Configure AutoEP expert parallelism for MoE models. AutoEP automatically detects MoE layers in HuggingFace models and replaces them with EP-enabled versions using TorchTitan's grouped GEMM kernels. Requires zero model code changes. Supports ZeRO stages 0, 1, 2, and constrained ZeRO Stage 3.
881911
```json

0 commit comments

Comments
 (0)