Spaces:
Running on Zero
Running on Zero
File size: 6,753 Bytes
4a8de45 | 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 | """Batch-1 static CUDA Graph cache for the HF text encoder."""
from __future__ import annotations
import threading
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
import torch
@dataclass
class _GraphRecord:
graph: torch.cuda.CUDAGraph
stream: torch.cuda.Stream
input_ids: torch.Tensor
attention_mask: torch.Tensor
position_ids: torch.Tensor
output: torch.Tensor
class TextEncoderGraphCache:
"""Capture fixed padded text-encoder buckets and replay them without CPU dispatch."""
def __init__(
self, text_encoder: torch.nn.Module, *, token_granularity: int = 32
) -> None:
if token_granularity <= 0:
raise ValueError("token_granularity must be > 0")
self.text_encoder = text_encoder
self.text_encoder._force_static_attention_mask = True
self.text_encoder.config._attn_implementation = "sdpa"
self.token_granularity = int(token_granularity)
self._records: dict[tuple[int, int], _GraphRecord] = {}
self._lock = threading.RLock()
self._graph_pool = torch.cuda.graph_pool_handle()
self.captures = 0
self.replays = 0
self._frozen = False
def _bucket(self, length: int) -> int:
return (
(int(length) + self.token_granularity - 1) // self.token_granularity
) * self.token_granularity
@staticmethod
def _smallest_fitting_key(
keys: Sequence[tuple[int, int]],
*,
batch_size: int,
token_length: int,
) -> tuple[int, int] | None:
candidates = [
key
for key in keys
if key[0] == batch_size and key[1] >= token_length
]
return min(candidates, key=lambda key: key[1], default=None)
@staticmethod
def _copy_inputs(
segments: Sequence[torch.Tensor],
input_ids: torch.Tensor,
attention_mask: torch.Tensor,
position_ids: torch.Tensor,
) -> None:
input_ids.zero_()
attention_mask.zero_()
position_ids.zero_()
for row, segment in enumerate(segments):
length = int(segment.shape[0])
input_ids[row, :length].copy_(segment)
attention_mask[row, :length].fill_(1)
position_ids[row, :length].copy_(
torch.arange(length, device=segment.device, dtype=torch.long)
)
@torch.inference_mode()
def __call__(
self, segments: Sequence[torch.Tensor]
) -> tuple[list[torch.Tensor], list[Any]]:
if not segments:
return [], []
lengths = [int(segment.shape[0]) for segment in segments]
if any(length <= 0 for length in lengths):
raise ValueError("text encoder graph does not support empty segments")
batch_size = len(segments)
max_length = self._bucket(max(lengths))
requested_key = (batch_size, max_length)
with self._lock:
key = self._smallest_fitting_key(
self._records,
batch_size=batch_size,
token_length=max_length,
)
record = self._records.get(key) if key is not None else None
if record is None:
if self._frozen:
raise RuntimeError(
f"text encoder CUDA graph {requested_key} has no fitting bucket "
"in the warmup profile"
)
key = requested_key
device = segments[0].device
static_ids = torch.zeros(
(batch_size, max_length), dtype=segments[0].dtype, device=device
)
static_mask = torch.zeros(
(batch_size, max_length), dtype=torch.long, device=device
)
static_positions = torch.zeros_like(static_mask)
self._copy_inputs(segments, static_ids, static_mask, static_positions)
capture_stream = torch.cuda.Stream(device=device)
capture_stream.wait_stream(torch.cuda.current_stream(device))
with torch.cuda.stream(capture_stream):
for _ in range(3):
static_output = self.text_encoder(
input_ids=static_ids,
attention_mask=static_mask,
position_ids=static_positions,
output_hidden_states=False,
).last_hidden_state
capture_stream.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(
graph,
stream=capture_stream,
pool=self._graph_pool,
capture_error_mode="thread_local",
):
static_output = self.text_encoder(
input_ids=static_ids,
attention_mask=static_mask,
position_ids=static_positions,
output_hidden_states=False,
).last_hidden_state
record = _GraphRecord(
graph=graph,
stream=capture_stream,
input_ids=static_ids,
attention_mask=static_mask,
position_ids=static_positions,
output=static_output,
)
self._records[key] = record
self.captures += 1
self._copy_inputs(
segments,
record.input_ids,
record.attention_mask,
record.position_ids,
)
current_stream = torch.cuda.current_stream(segments[0].device)
record.stream.wait_stream(current_stream)
record.graph.replay()
current_stream.wait_stream(record.stream)
self.replays += 1
hidden_states = [
record.output[row, :length] for row, length in enumerate(lengths)
]
return hidden_states, []
@property
def graph_keys(self) -> tuple[tuple[int, int], ...]:
return tuple(sorted(self._records))
@torch.inference_mode()
def warmup_graph(
self, *, batch_size: int, token_length: int, device: torch.device
) -> None:
if self._frozen:
raise RuntimeError("text encoder CUDA graph cache is already frozen")
segments = [
torch.zeros(token_length, dtype=torch.long, device=device)
for _ in range(batch_size)
]
self(segments)
def freeze(self) -> None:
self._frozen = True
|