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.
Files changed (21) hide show
  1. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/PKG-INFO +56 -2
  2. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/README.md +54 -0
  3. cortexgrid_infer-0.2.2/cortexgrid_infer/compiling.py +70 -0
  4. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/huggingface_complete_serve.py +15 -1
  5. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/huggingface_image_serve.py +5 -0
  6. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/pyproject.toml +4 -2
  7. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/.gitignore +0 -0
  8. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/LICENSE +0 -0
  9. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/__init__.py +0 -0
  10. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/completion.py +0 -0
  11. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/core.py +0 -0
  12. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/device.py +0 -0
  13. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/importing.py +0 -0
  14. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/__init__.py +0 -0
  15. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/anthropic.py +0 -0
  16. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/anthropic_serve.py +0 -0
  17. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/huggingface_complete.py +0 -0
  18. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/providers/huggingface_image.py +0 -0
  19. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/py.typed +0 -0
  20. {cortexgrid_infer-0.2.1 → cortexgrid_infer-0.2.2}/cortexgrid_infer/requirements.py +0 -0
  21. {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.1
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.40.0
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 AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
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.1"
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
- "transformers>=4.40.0",
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",