cortexgrid-infer 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/PKG-INFO +56 -2
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/README.md +54 -0
- cortexgrid_infer-0.2.2/cortexgrid_infer/compiling.py +70 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/huggingface_complete_serve.py +15 -1
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/huggingface_image_serve.py +5 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/pyproject.toml +4 -2
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/.gitignore +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/LICENSE +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/__init__.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/completion.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/core.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/device.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/importing.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/__init__.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/anthropic.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/anthropic_serve.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/huggingface_complete.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/huggingface_image.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/py.typed +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/requirements.py +0 -0
- {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cortexgrid-infer
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Deploy and interact with models across providers (Anthropic, HuggingFace LLM + diffusion)
|
|
5
5
|
Project-URL: Homepage, https://github.com/robodatalab/cortexgrid-infer
|
|
6
6
|
Project-URL: Repository, https://github.com/robodatalab/cortexgrid-infer
|
|
@@ -19,7 +19,7 @@ Requires-Dist: protobuf>=4.0
|
|
|
19
19
|
Requires-Dist: python-dotenv>=1.0.0
|
|
20
20
|
Requires-Dist: sentencepiece>=0.2
|
|
21
21
|
Requires-Dist: torch>=2.0.0
|
|
22
|
-
Requires-Dist: transformers>=4.
|
|
22
|
+
Requires-Dist: transformers>=4.47.0
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
@@ -149,6 +149,58 @@ the whole model onto a single device. A model too large for any one card needs a
|
|
|
149
149
|
app that shards it, not a larger `num_gpus`: the requirement would simply never be
|
|
150
150
|
placed. (`AnthropicImport` asks for none — it holds no weights.)
|
|
151
151
|
|
|
152
|
+
## Compilation
|
|
153
|
+
|
|
154
|
+
Both HuggingFace serve apps run `torch.compile` over the model they load, because
|
|
155
|
+
eager PyTorch bills per *operation* and a transformer forward is mostly glue —
|
|
156
|
+
views, broadcasts, permutes, elementwise adds — with only a small fraction of the
|
|
157
|
+
dispatches being the matrix multiplies that do the arithmetic. While the tensors
|
|
158
|
+
are large the host stays ahead of the accelerator and none of that shows; once
|
|
159
|
+
they are small, the accelerator drains its queue faster than the host can fill it
|
|
160
|
+
and step time tracks the operation count rather than the work. Feeding the model
|
|
161
|
+
less then stops helping, because the op count does not shrink with the input.
|
|
162
|
+
Inductor fuses the glue away; `reduce-overhead` additionally captures a CUDA graph,
|
|
163
|
+
so a step replays one graph instead of thousands of launches.
|
|
164
|
+
|
|
165
|
+
`cortexgrid_infer.compiling` offers two modes, and a serve app picks the one that
|
|
166
|
+
fits the work it does:
|
|
167
|
+
|
|
168
|
+
| | | for |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `Mode.GRAPHED` (`reduce-overhead`) | fuses **and** captures a CUDA graph, so a forward replays one graph instead of issuing its launches | a loop of identically shaped forwards, where the capture is reused every iteration. A graph cannot go dynamic, so each distinct shape captures separately |
|
|
171
|
+
| `Mode.FUSED` (`default`) | fuses only | work whose shapes move under it, where there is nothing stable to capture and dynamo is free to compile one dynamic kernel set for all of them |
|
|
172
|
+
|
|
173
|
+
Both serve apps are tuned rather than defaulted:
|
|
174
|
+
|
|
175
|
+
- **`HuggingFaceImageDeployment`** compiles the denoiser (`transformer` or `unet`)
|
|
176
|
+
and every `text_encoder*` as `Mode.GRAPHED`. A denoising schedule is the ideal case —
|
|
177
|
+
every step is the same shapes — so the capture is made once and replayed for the
|
|
178
|
+
whole loop. The VAE is left alone: smallest win of the three, and the one whose
|
|
179
|
+
shapes move most, especially with tiling on.
|
|
180
|
+
- **`HuggingFaceCompletingDeployment`** does *not* compile the module. Decode has
|
|
181
|
+
no stable shape until the KV cache is static, so it sets
|
|
182
|
+
`cache_implementation="static"` and a `CompileConfig(mode=Mode.GRAPHED)`, and
|
|
183
|
+
transformers compiles `generate` itself — capturing prefill and decode
|
|
184
|
+
separately, which it is better placed to do. Compiling the module here as well
|
|
185
|
+
would only compile the same forward twice. The cost is VRAM: a static cache is
|
|
186
|
+
preallocated to the length asked for, where a dynamic one grows into it.
|
|
187
|
+
|
|
188
|
+
Neither asks to be configured, for the same reason the serve apps do not ask which
|
|
189
|
+
dtype to load in. Both happen on CUDA and nowhere else — inductor is weakest off
|
|
190
|
+
it, and a host outrunning its accelerator is a GPU problem to begin with.
|
|
191
|
+
|
|
192
|
+
Two things worth knowing:
|
|
193
|
+
|
|
194
|
+
- **Warm-up is per input shape, and lands on a request.** Compilation is lazy, so
|
|
195
|
+
the first request at each image size — or each prompt and generation length —
|
|
196
|
+
pays for it.
|
|
197
|
+
- **Compilation never becomes load-bearing.** A backend that cannot compile a model
|
|
198
|
+
falls back to eager rather than failing the deployment.
|
|
199
|
+
|
|
200
|
+
It does not fix everything. Every forward re-reads the model's whole weight tensor
|
|
201
|
+
from memory, and no amount of fusion makes that read smaller. Below some input size
|
|
202
|
+
it is the floor; only quantizing the weights or running fewer forwards moves it.
|
|
203
|
+
|
|
152
204
|
## Inference
|
|
153
205
|
|
|
154
206
|
`complete` streams `CompletionChunk`s (`.content`, `.tool_calls`, `.finish_reason`):
|
|
@@ -215,6 +267,8 @@ serve app re-encodes them into the text form the client parses.
|
|
|
215
267
|
| `HuggingFaceImageModel(url, model_id)` | Client for a deployed diffusers app. |
|
|
216
268
|
| `split_model_id(model_id) -> (family, suffix)` | The registry identity an importer derives. |
|
|
217
269
|
| `detect_device()` | The torch device a serve app should load onto. |
|
|
270
|
+
| `compiling.Mode.GRAPHED` / `compiling.Mode.FUSED` | The two modes a serve app chooses between. See [Compilation](#compilation). |
|
|
271
|
+
| `compiling.supported(device)` / `compile_module(module, device, mode)` / `compile_pipeline(pipe, device, mode)` | Whether to compile here, and what a serve app calls to do it. |
|
|
218
272
|
| `ModelDeployFailed` | Re-exported from cortexgrid; subclasses `RuntimeError`. |
|
|
219
273
|
|
|
220
274
|
Types: `DeployedModel`, `CompletingModel`, `GeneratingModel`, `CompletionChunk`,
|
|
@@ -123,6 +123,58 @@ the whole model onto a single device. A model too large for any one card needs a
|
|
|
123
123
|
app that shards it, not a larger `num_gpus`: the requirement would simply never be
|
|
124
124
|
placed. (`AnthropicImport` asks for none — it holds no weights.)
|
|
125
125
|
|
|
126
|
+
## Compilation
|
|
127
|
+
|
|
128
|
+
Both HuggingFace serve apps run `torch.compile` over the model they load, because
|
|
129
|
+
eager PyTorch bills per *operation* and a transformer forward is mostly glue —
|
|
130
|
+
views, broadcasts, permutes, elementwise adds — with only a small fraction of the
|
|
131
|
+
dispatches being the matrix multiplies that do the arithmetic. While the tensors
|
|
132
|
+
are large the host stays ahead of the accelerator and none of that shows; once
|
|
133
|
+
they are small, the accelerator drains its queue faster than the host can fill it
|
|
134
|
+
and step time tracks the operation count rather than the work. Feeding the model
|
|
135
|
+
less then stops helping, because the op count does not shrink with the input.
|
|
136
|
+
Inductor fuses the glue away; `reduce-overhead` additionally captures a CUDA graph,
|
|
137
|
+
so a step replays one graph instead of thousands of launches.
|
|
138
|
+
|
|
139
|
+
`cortexgrid_infer.compiling` offers two modes, and a serve app picks the one that
|
|
140
|
+
fits the work it does:
|
|
141
|
+
|
|
142
|
+
| | | for |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `Mode.GRAPHED` (`reduce-overhead`) | fuses **and** captures a CUDA graph, so a forward replays one graph instead of issuing its launches | a loop of identically shaped forwards, where the capture is reused every iteration. A graph cannot go dynamic, so each distinct shape captures separately |
|
|
145
|
+
| `Mode.FUSED` (`default`) | fuses only | work whose shapes move under it, where there is nothing stable to capture and dynamo is free to compile one dynamic kernel set for all of them |
|
|
146
|
+
|
|
147
|
+
Both serve apps are tuned rather than defaulted:
|
|
148
|
+
|
|
149
|
+
- **`HuggingFaceImageDeployment`** compiles the denoiser (`transformer` or `unet`)
|
|
150
|
+
and every `text_encoder*` as `Mode.GRAPHED`. A denoising schedule is the ideal case —
|
|
151
|
+
every step is the same shapes — so the capture is made once and replayed for the
|
|
152
|
+
whole loop. The VAE is left alone: smallest win of the three, and the one whose
|
|
153
|
+
shapes move most, especially with tiling on.
|
|
154
|
+
- **`HuggingFaceCompletingDeployment`** does *not* compile the module. Decode has
|
|
155
|
+
no stable shape until the KV cache is static, so it sets
|
|
156
|
+
`cache_implementation="static"` and a `CompileConfig(mode=Mode.GRAPHED)`, and
|
|
157
|
+
transformers compiles `generate` itself — capturing prefill and decode
|
|
158
|
+
separately, which it is better placed to do. Compiling the module here as well
|
|
159
|
+
would only compile the same forward twice. The cost is VRAM: a static cache is
|
|
160
|
+
preallocated to the length asked for, where a dynamic one grows into it.
|
|
161
|
+
|
|
162
|
+
Neither asks to be configured, for the same reason the serve apps do not ask which
|
|
163
|
+
dtype to load in. Both happen on CUDA and nowhere else — inductor is weakest off
|
|
164
|
+
it, and a host outrunning its accelerator is a GPU problem to begin with.
|
|
165
|
+
|
|
166
|
+
Two things worth knowing:
|
|
167
|
+
|
|
168
|
+
- **Warm-up is per input shape, and lands on a request.** Compilation is lazy, so
|
|
169
|
+
the first request at each image size — or each prompt and generation length —
|
|
170
|
+
pays for it.
|
|
171
|
+
- **Compilation never becomes load-bearing.** A backend that cannot compile a model
|
|
172
|
+
falls back to eager rather than failing the deployment.
|
|
173
|
+
|
|
174
|
+
It does not fix everything. Every forward re-reads the model's whole weight tensor
|
|
175
|
+
from memory, and no amount of fusion makes that read smaller. Below some input size
|
|
176
|
+
it is the floor; only quantizing the weights or running fewer forwards moves it.
|
|
177
|
+
|
|
126
178
|
## Inference
|
|
127
179
|
|
|
128
180
|
`complete` streams `CompletionChunk`s (`.content`, `.tool_calls`, `.finish_reason`):
|
|
@@ -189,6 +241,8 @@ serve app re-encodes them into the text form the client parses.
|
|
|
189
241
|
| `HuggingFaceImageModel(url, model_id)` | Client for a deployed diffusers app. |
|
|
190
242
|
| `split_model_id(model_id) -> (family, suffix)` | The registry identity an importer derives. |
|
|
191
243
|
| `detect_device()` | The torch device a serve app should load onto. |
|
|
244
|
+
| `compiling.Mode.GRAPHED` / `compiling.Mode.FUSED` | The two modes a serve app chooses between. See [Compilation](#compilation). |
|
|
245
|
+
| `compiling.supported(device)` / `compile_module(module, device, mode)` / `compile_pipeline(pipe, device, mode)` | Whether to compile here, and what a serve app calls to do it. |
|
|
192
246
|
| `ModelDeployFailed` | Re-exported from cortexgrid; subclasses `RuntimeError`. |
|
|
193
247
|
|
|
194
248
|
Types: `DeployedModel`, `CompletingModel`, `GeneratingModel`, `CompletionChunk`,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""`torch.compile` for the serve apps that run a model in-process."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Inductor is weakest off CUDA, and a host outrunning its accelerator is a GPU problem.
|
|
12
|
+
_ACCELERATOR = "cuda"
|
|
13
|
+
|
|
14
|
+
# Parts of a diffusers pipeline worth compiling. Not the VAE: smallest win, shakiest shapes.
|
|
15
|
+
_DENOISER_ATTRS = ("transformer", "unet")
|
|
16
|
+
_TEXT_ENCODER_ATTRS = ("text_encoder", "text_encoder_2", "text_encoder_3")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Mode(StrEnum):
|
|
20
|
+
"""How far `torch.compile` should go. A serve app picks by the shape of its work."""
|
|
21
|
+
|
|
22
|
+
GRAPHED = "reduce-overhead"
|
|
23
|
+
"""Fuse, then capture a CUDA graph. For a loop of identically shaped forwards;
|
|
24
|
+
captures per shape, so it wants a settled set of them."""
|
|
25
|
+
|
|
26
|
+
FUSED = "default"
|
|
27
|
+
"""Fuse only. For work whose shapes move under it, leaving dynamo free to
|
|
28
|
+
compile one dynamic kernel set for all of them."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def supported(device: Any) -> bool:
|
|
32
|
+
"""Whether compiling is worth doing on this device."""
|
|
33
|
+
return getattr(device, "type", None) == _ACCELERATOR
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def compile_module(module: Any, device: Any, mode: Mode, label: str = "model") -> bool:
|
|
37
|
+
"""Compile one module's forward, in place. Returns whether it was compiled.
|
|
38
|
+
|
|
39
|
+
Lazily, as `torch.compile` always is: the first forward through each input
|
|
40
|
+
shape is the one that pays."""
|
|
41
|
+
if module is None or not supported(device):
|
|
42
|
+
return False
|
|
43
|
+
try:
|
|
44
|
+
_fall_back_to_eager_on_failure()
|
|
45
|
+
module.compile(mode=mode)
|
|
46
|
+
except Exception as exc: # noqa: BLE001 - an optimisation, never fatal
|
|
47
|
+
log.warning("torch.compile(%s, mode=%s) failed: %s", label, mode, exc)
|
|
48
|
+
return False
|
|
49
|
+
log.info("compiling %s with torch.compile(mode=%s)", label, mode)
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def compile_pipeline(pipe: Any, device: Any, mode: Mode) -> list[str]:
|
|
54
|
+
"""Compile a diffusers pipeline's denoiser and text encoders. Returns which."""
|
|
55
|
+
compiled = []
|
|
56
|
+
for attr in (*_DENOISER_ATTRS, *_TEXT_ENCODER_ATTRS):
|
|
57
|
+
module = getattr(pipe, attr, None)
|
|
58
|
+
if module is not None and compile_module(module, device, mode, label=attr):
|
|
59
|
+
compiled.append(attr)
|
|
60
|
+
return compiled
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _fall_back_to_eager_on_failure() -> None:
|
|
64
|
+
"""Make dynamo warn rather than raise out of a request. Process-wide."""
|
|
65
|
+
try:
|
|
66
|
+
import torch._dynamo.config as dynamo_config
|
|
67
|
+
|
|
68
|
+
dynamo_config.suppress_errors = True
|
|
69
|
+
except Exception: # noqa: BLE001 - best effort on a private flag
|
|
70
|
+
pass
|
|
@@ -11,9 +11,15 @@ import cortexgrid
|
|
|
11
11
|
from cortexgrid import serve
|
|
12
12
|
from fastapi import FastAPI
|
|
13
13
|
from fastapi.responses import StreamingResponse
|
|
14
|
-
from transformers import
|
|
14
|
+
from transformers import (
|
|
15
|
+
AutoModelForCausalLM,
|
|
16
|
+
AutoTokenizer,
|
|
17
|
+
CompileConfig,
|
|
18
|
+
TextIteratorStreamer,
|
|
19
|
+
)
|
|
15
20
|
import torch
|
|
16
21
|
|
|
22
|
+
from cortexgrid_infer import compiling
|
|
17
23
|
from cortexgrid_infer.device import detect_device
|
|
18
24
|
|
|
19
25
|
|
|
@@ -43,6 +49,14 @@ class HuggingFaceCompletingDeployment:
|
|
|
43
49
|
str(path), torch_dtype=torch.float16
|
|
44
50
|
)
|
|
45
51
|
self._model.to(self._device) # type: ignore
|
|
52
|
+
# Decode has no stable shape to capture until the cache is static; given one,
|
|
53
|
+
# transformers compiles `generate` itself, so compiling the module would double up.
|
|
54
|
+
self._compiled = compiling.supported(self._device)
|
|
55
|
+
if self._compiled:
|
|
56
|
+
self._model.generation_config.cache_implementation = "static"
|
|
57
|
+
self._model.generation_config.compile_config = CompileConfig(
|
|
58
|
+
mode=compiling.Mode.GRAPHED
|
|
59
|
+
)
|
|
46
60
|
|
|
47
61
|
@_app.post("/complete")
|
|
48
62
|
async def complete(self, body: dict[str, Any]) -> StreamingResponse:
|
|
@@ -25,6 +25,7 @@ from fastapi import FastAPI
|
|
|
25
25
|
from PIL import Image, ImageOps
|
|
26
26
|
import torch
|
|
27
27
|
|
|
28
|
+
from cortexgrid_infer import compiling
|
|
28
29
|
from cortexgrid_infer.device import detect_device
|
|
29
30
|
|
|
30
31
|
|
|
@@ -70,6 +71,10 @@ class HuggingFaceImageDeployment:
|
|
|
70
71
|
if vae is not None and hasattr(vae, "enable_tiling"):
|
|
71
72
|
vae.enable_tiling()
|
|
72
73
|
self._pipe = pipe
|
|
74
|
+
# A denoising schedule is a loop of identically shaped forwards: capture once, replay.
|
|
75
|
+
self._compiled = compiling.compile_pipeline(
|
|
76
|
+
pipe, self._device, compiling.Mode.GRAPHED
|
|
77
|
+
)
|
|
73
78
|
|
|
74
79
|
@_app.post("/generate")
|
|
75
80
|
async def generate(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cortexgrid-infer"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Deploy and interact with models across providers (Anthropic, HuggingFace LLM + diffusion)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -17,7 +17,9 @@ dependencies = [
|
|
|
17
17
|
# redundant PUT - which is why this library has no reuse guard of its own.
|
|
18
18
|
"cortexgrid>=0.3.4",
|
|
19
19
|
"torch>=2.0.0",
|
|
20
|
-
|
|
20
|
+
# 4.47: CompileConfig, which the completion serve app hands to `generate`
|
|
21
|
+
# alongside a static cache so transformers compiles the decode step itself.
|
|
22
|
+
"transformers>=4.47.0",
|
|
21
23
|
"huggingface_hub>=0.20.0",
|
|
22
24
|
"anthropic>=0.40.0",
|
|
23
25
|
"python-dotenv>=1.0.0",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/anthropic_serve.py
RENAMED
|
File without changes
|
{cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/huggingface_complete.py
RENAMED
|
File without changes
|
{cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/huggingface_image.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|