cortexgrid-infer 0.3.4__tar.gz → 0.3.6__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 (32) hide show
  1. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/PKG-INFO +49 -15
  2. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/README.md +47 -13
  3. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/serve_apps/anthropic.py +5 -4
  4. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/serve_apps/base.py +7 -1
  5. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/serve_apps/gemini/base.py +4 -4
  6. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/serve_apps/image2mesh.py +21 -3
  7. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/serve_apps/text2image.py +9 -5
  8. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/serve_apps/text2text.py +13 -6
  9. cortexgrid_infer-0.3.6/cortexgrid_infer/serve_apps/text_rewriter.py +107 -0
  10. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/pyproject.toml +4 -2
  11. cortexgrid_infer-0.3.4/cortexgrid_infer/serve_apps/text_rewriter.py +0 -54
  12. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/.gitignore +0 -0
  13. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/LICENSE +0 -0
  14. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/__init__.py +0 -0
  15. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/compiling.py +0 -0
  16. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/core.py +0 -0
  17. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/device.py +0 -0
  18. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/importers/__init__.py +0 -0
  19. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/importers/base.py +0 -0
  20. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/importers/huggingface.py +0 -0
  21. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/protocols/__init__.py +0 -0
  22. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/protocols/completion.py +0 -0
  23. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/protocols/imaging.py +0 -0
  24. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/protocols/meshing.py +0 -0
  25. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/protocols/rewriting.py +0 -0
  26. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/py.typed +0 -0
  27. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/registry.py +0 -0
  28. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/serve_apps/__init__.py +0 -0
  29. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/serve_apps/gemini/__init__.py +0 -0
  30. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/serve_apps/gemini/text2image.py +0 -0
  31. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/serve_apps/gemini/text2text.py +0 -0
  32. {cortexgrid_infer-0.3.4 → cortexgrid_infer-0.3.6}/cortexgrid_infer/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cortexgrid-infer
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: Serve apps per task (text-to-text, text-to-image, image-to-mesh) and importers per source (HuggingFace) for running models on cortexgrid
5
5
  Project-URL: Homepage, https://github.com/robodatalab/cortexgrid-infer
6
6
  Project-URL: Repository, https://github.com/robodatalab/cortexgrid-infer
@@ -9,7 +9,7 @@ License-File: LICENSE
9
9
  Requires-Python: <3.12,>=3.11
10
10
  Requires-Dist: accelerate>=0.33
11
11
  Requires-Dist: anthropic>=0.40.0
12
- Requires-Dist: cortexgrid>=0.3.4
12
+ Requires-Dist: cortexgrid>=0.3.12
13
13
  Requires-Dist: diffusers>=0.38
14
14
  Requires-Dist: fastapi>=0.133
15
15
  Requires-Dist: google-genai>=1.0
@@ -132,7 +132,7 @@ async def run():
132
132
  asyncio.run(run())
133
133
 
134
134
  # 4. Teardown.
135
- cortexgrid.undeploy_model(imp.family, imp.suffix, cortexgrid.IMPORTED) # free the GPU
135
+ cortexgrid.undeploy_model(deployment.key) # free the GPU
136
136
  cortexgrid.delete_model(imp.family, imp.suffix, cortexgrid.IMPORTED) # and the weights
137
137
  ```
138
138
 
@@ -178,7 +178,27 @@ beyond the weights. (A `Hosted` entry asks for none — it holds no weights.)
178
178
 
179
179
  ## Compilation
180
180
 
181
- `Text2Text` and `Text2Image` run `torch.compile` over the model they load, because
181
+ Every serve app that runs weights can run them through `torch.compile`, and none
182
+ does unless asked: a replica is eager unless its deployment's config carries
183
+ `compile: "true"`. Each `LocalModel` writes `compile: "false"` onto the model card
184
+ at import, so turning it on is a deployment that asks for it:
185
+
186
+ ```python
187
+ cortexgrid.deploy_model(family, suffix, cortexgrid.IMPORTED, wait=True, config={"compile": "true"})
188
+ ```
189
+
190
+ That is a separate, compiled deployment of the same model, alongside any uncompiled
191
+ one: a model gets one deployment per distinct config, each at its own
192
+ `deployment.url`. `Text2Text`'s `enable_thinking` and `max_total_tokens` are set per
193
+ deployment the same way. Whatever a deployment leaves out comes from the model card,
194
+ so the defaults `config()` writes there at import still apply.
195
+
196
+ It is off by default because it is not free: every input shape pays a warm-up on
197
+ the request that first brings it, and each CUDA graph holds memory of its own. It
198
+ pays back where traffic keeps to a few shapes and the model is small enough for
199
+ launch overhead to matter. A deployment asking for it off CUDA is served eager.
200
+
201
+ Compiled, a serve app runs `torch.compile` over the model it loads, because
182
202
  eager PyTorch bills per *operation* and a transformer forward is mostly glue —
183
203
  views, broadcasts, permutes, elementwise adds — with only a small fraction of the
184
204
  dispatches being the matrix multiplies that do the arithmetic. While the tensors
@@ -197,7 +217,7 @@ fits the work it does:
197
217
  | `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 |
198
218
  | `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 |
199
219
 
200
- Both serve apps are tuned rather than defaulted:
220
+ Each serve app is tuned rather than defaulted:
201
221
 
202
222
  - **`Text2Image`** compiles the denoiser (`transformer` or `unet`)
203
223
  and every `text_encoder*` as `Mode.GRAPHED`. A denoising schedule is the ideal case —
@@ -217,19 +237,32 @@ Both serve apps are tuned rather than defaulted:
217
237
  1.2 GB of keys and values re-read per generated token, several times the
218
238
  traffic of the model's own weights, to make room for a prompt nobody sent. It
219
239
  is pinned instead to the prompt and reply a caller plausibly sends: 4096 tokens,
220
- or `max_total_tokens` on the model card, never past the model's own
240
+ or `max_total_tokens` from the deployment's config, never past the model's own
221
241
  `max_position_embeddings`. A reply takes at most half of it, so an
222
242
  over-optimistic `max_new_tokens` cannot squeeze the prompt to nothing, and a
223
243
  prompt that will not fit in the rest is truncated — keeping the end, since a
224
244
  chat template puts the turn to answer last — with a warning naming both
225
245
  lengths. Serving it whole would resize the cache and recompile the decode loop,
226
246
  which costs more than the generation itself.
227
-
228
- The image app asks to be configured for nothing, for the same reason neither asks
229
- which dtype to load in; the completion app takes `max_total_tokens` alone, because
230
- how much cache a caller needs is the one thing the model cannot tell it. Both
231
- happen on CUDA and nowhere else — inductor is weakest off it, and a host
232
- outrunning its accelerator is a GPU problem to begin with.
247
+ - **`TextRewriter`** decodes as `Text2Text` does — a static cache, `Mode.GRAPHED`,
248
+ transformers compiling the step — with the cache sized to the 512 new tokens a
249
+ reply may take, and a larger `max_new_tokens` capped to it. An encoder-decoder
250
+ has a second cache, though: `generate` sizes cross-attention to the encoded
251
+ text, so each distinct text length would capture the decode loop afresh. The
252
+ text is therefore padded up to the next of 32, 64, 128, 256 or 512 tokens —
253
+ one capture per bucket, the padding masked out — and a longer one is cut to
254
+ 512, keeping its start, with a warning naming both lengths.
255
+ - **`Image2Mesh`** cannot know what to compile: each subclass brings its own
256
+ model. It calls the subclass's `compile(device)` after `load`, which does
257
+ nothing unless overridden; a subclass compiles its own modules there with
258
+ `compiling.compile_module`, in the mode that fits them.
259
+
260
+ Beyond the switch, the image app asks to be configured for nothing, for the same
261
+ reason none asks which dtype to load in; the completion app takes
262
+ `max_total_tokens` alone, because how much cache a caller needs is the one thing
263
+ the model cannot tell it. All of it happens on CUDA and nowhere else — inductor
264
+ is weakest off it, and a host outrunning its accelerator is a GPU problem to
265
+ begin with.
233
266
 
234
267
  Two things worth knowing:
235
268
 
@@ -338,10 +371,10 @@ diffusion settings with no Gemini counterpart, and come back as `None`.
338
371
  | `Importer` | Base of the importers: identity, scratch directory, `source`, and `requirements()` / `client(url)` taken from the serve app. A new source implements `download(local_dir)` and `weights()`. |
339
372
  | `Hosted(model_id, serve_app, **settings)` | Entry for a model hosted elsewhere; no weights. `settings` are the serve app's `config` arguments. |
340
373
  | `ModelEntry` | Base of both: `model_id`, `family`, `suffix`, `serve_app`, `requirements()`, `config()`, `client(url)`. |
341
- | `Text2Text`, `Text2Image`, `Image2Mesh` | Serve apps that run weights (`LocalModel`s). |
374
+ | `Text2Text`, `Text2Image`, `TextRewriter`, `Image2Mesh` | Serve apps that run weights (`LocalModel`s). |
342
375
  | `AnthropicText2Text` | Serve app that forwards to the Anthropic API (a `HostedModel`). |
343
376
  | `GeminiText2Text`, `GeminiText2Image` | Serve apps that forward to the Gemini API (`HostedModel`s sharing the `GeminiModel` base). |
344
- | `LocalModel` | Base of the serve apps that run weights: `ignore_patterns()`, `bytes_per_param`, `min_vram_gb`, `requirements(weights)`, `client(url, name)`. |
377
+ | `LocalModel` | Base of the serve apps that run weights: `ignore_patterns()`, `bytes_per_param`, `min_vram_gb`, `requirements(weights)`, `config()` (the model card's defaults, `compile: "false"` among them), `client(url, name)`. |
345
378
  | `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`, `client(url, name)`. |
346
379
  | `Weights(params=None, file_bytes=0)` | What an importer reads about the weights, handed to the serve app to size a replica. |
347
380
  | `complete(model, messages, tools=None, max_new_tokens=2048, temperature=0.7, **kw)` | Async stream of `CompletionChunk` for a `CompletingModel`. |
@@ -370,7 +403,8 @@ Extending it:
370
403
  (there is an `encode_tool_call` for upstreams that report them structurally) —
371
404
  then `ServedCompletingModel` is its client, unchanged.
372
405
  - **A model no task's serve app can load** gets its own serve app — an `Image2Mesh`
373
- subclass, say — and, if its source is unusual too, its own importer.
406
+ subclass, say, with `load` and `make_mesh`, and `compile` if compiling it pays —
407
+ and, if its source is unusual too, its own importer.
374
408
 
375
409
  ## Notes
376
410
 
@@ -104,7 +104,7 @@ async def run():
104
104
  asyncio.run(run())
105
105
 
106
106
  # 4. Teardown.
107
- cortexgrid.undeploy_model(imp.family, imp.suffix, cortexgrid.IMPORTED) # free the GPU
107
+ cortexgrid.undeploy_model(deployment.key) # free the GPU
108
108
  cortexgrid.delete_model(imp.family, imp.suffix, cortexgrid.IMPORTED) # and the weights
109
109
  ```
110
110
 
@@ -150,7 +150,27 @@ beyond the weights. (A `Hosted` entry asks for none — it holds no weights.)
150
150
 
151
151
  ## Compilation
152
152
 
153
- `Text2Text` and `Text2Image` run `torch.compile` over the model they load, because
153
+ Every serve app that runs weights can run them through `torch.compile`, and none
154
+ does unless asked: a replica is eager unless its deployment's config carries
155
+ `compile: "true"`. Each `LocalModel` writes `compile: "false"` onto the model card
156
+ at import, so turning it on is a deployment that asks for it:
157
+
158
+ ```python
159
+ cortexgrid.deploy_model(family, suffix, cortexgrid.IMPORTED, wait=True, config={"compile": "true"})
160
+ ```
161
+
162
+ That is a separate, compiled deployment of the same model, alongside any uncompiled
163
+ one: a model gets one deployment per distinct config, each at its own
164
+ `deployment.url`. `Text2Text`'s `enable_thinking` and `max_total_tokens` are set per
165
+ deployment the same way. Whatever a deployment leaves out comes from the model card,
166
+ so the defaults `config()` writes there at import still apply.
167
+
168
+ It is off by default because it is not free: every input shape pays a warm-up on
169
+ the request that first brings it, and each CUDA graph holds memory of its own. It
170
+ pays back where traffic keeps to a few shapes and the model is small enough for
171
+ launch overhead to matter. A deployment asking for it off CUDA is served eager.
172
+
173
+ Compiled, a serve app runs `torch.compile` over the model it loads, because
154
174
  eager PyTorch bills per *operation* and a transformer forward is mostly glue —
155
175
  views, broadcasts, permutes, elementwise adds — with only a small fraction of the
156
176
  dispatches being the matrix multiplies that do the arithmetic. While the tensors
@@ -169,7 +189,7 @@ fits the work it does:
169
189
  | `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 |
170
190
  | `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 |
171
191
 
172
- Both serve apps are tuned rather than defaulted:
192
+ Each serve app is tuned rather than defaulted:
173
193
 
174
194
  - **`Text2Image`** compiles the denoiser (`transformer` or `unet`)
175
195
  and every `text_encoder*` as `Mode.GRAPHED`. A denoising schedule is the ideal case —
@@ -189,19 +209,32 @@ Both serve apps are tuned rather than defaulted:
189
209
  1.2 GB of keys and values re-read per generated token, several times the
190
210
  traffic of the model's own weights, to make room for a prompt nobody sent. It
191
211
  is pinned instead to the prompt and reply a caller plausibly sends: 4096 tokens,
192
- or `max_total_tokens` on the model card, never past the model's own
212
+ or `max_total_tokens` from the deployment's config, never past the model's own
193
213
  `max_position_embeddings`. A reply takes at most half of it, so an
194
214
  over-optimistic `max_new_tokens` cannot squeeze the prompt to nothing, and a
195
215
  prompt that will not fit in the rest is truncated — keeping the end, since a
196
216
  chat template puts the turn to answer last — with a warning naming both
197
217
  lengths. Serving it whole would resize the cache and recompile the decode loop,
198
218
  which costs more than the generation itself.
199
-
200
- The image app asks to be configured for nothing, for the same reason neither asks
201
- which dtype to load in; the completion app takes `max_total_tokens` alone, because
202
- how much cache a caller needs is the one thing the model cannot tell it. Both
203
- happen on CUDA and nowhere else — inductor is weakest off it, and a host
204
- outrunning its accelerator is a GPU problem to begin with.
219
+ - **`TextRewriter`** decodes as `Text2Text` does — a static cache, `Mode.GRAPHED`,
220
+ transformers compiling the step — with the cache sized to the 512 new tokens a
221
+ reply may take, and a larger `max_new_tokens` capped to it. An encoder-decoder
222
+ has a second cache, though: `generate` sizes cross-attention to the encoded
223
+ text, so each distinct text length would capture the decode loop afresh. The
224
+ text is therefore padded up to the next of 32, 64, 128, 256 or 512 tokens —
225
+ one capture per bucket, the padding masked out — and a longer one is cut to
226
+ 512, keeping its start, with a warning naming both lengths.
227
+ - **`Image2Mesh`** cannot know what to compile: each subclass brings its own
228
+ model. It calls the subclass's `compile(device)` after `load`, which does
229
+ nothing unless overridden; a subclass compiles its own modules there with
230
+ `compiling.compile_module`, in the mode that fits them.
231
+
232
+ Beyond the switch, the image app asks to be configured for nothing, for the same
233
+ reason none asks which dtype to load in; the completion app takes
234
+ `max_total_tokens` alone, because how much cache a caller needs is the one thing
235
+ the model cannot tell it. All of it happens on CUDA and nowhere else — inductor
236
+ is weakest off it, and a host outrunning its accelerator is a GPU problem to
237
+ begin with.
205
238
 
206
239
  Two things worth knowing:
207
240
 
@@ -310,10 +343,10 @@ diffusion settings with no Gemini counterpart, and come back as `None`.
310
343
  | `Importer` | Base of the importers: identity, scratch directory, `source`, and `requirements()` / `client(url)` taken from the serve app. A new source implements `download(local_dir)` and `weights()`. |
311
344
  | `Hosted(model_id, serve_app, **settings)` | Entry for a model hosted elsewhere; no weights. `settings` are the serve app's `config` arguments. |
312
345
  | `ModelEntry` | Base of both: `model_id`, `family`, `suffix`, `serve_app`, `requirements()`, `config()`, `client(url)`. |
313
- | `Text2Text`, `Text2Image`, `Image2Mesh` | Serve apps that run weights (`LocalModel`s). |
346
+ | `Text2Text`, `Text2Image`, `TextRewriter`, `Image2Mesh` | Serve apps that run weights (`LocalModel`s). |
314
347
  | `AnthropicText2Text` | Serve app that forwards to the Anthropic API (a `HostedModel`). |
315
348
  | `GeminiText2Text`, `GeminiText2Image` | Serve apps that forward to the Gemini API (`HostedModel`s sharing the `GeminiModel` base). |
316
- | `LocalModel` | Base of the serve apps that run weights: `ignore_patterns()`, `bytes_per_param`, `min_vram_gb`, `requirements(weights)`, `client(url, name)`. |
349
+ | `LocalModel` | Base of the serve apps that run weights: `ignore_patterns()`, `bytes_per_param`, `min_vram_gb`, `requirements(weights)`, `config()` (the model card's defaults, `compile: "false"` among them), `client(url, name)`. |
317
350
  | `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`, `client(url, name)`. |
318
351
  | `Weights(params=None, file_bytes=0)` | What an importer reads about the weights, handed to the serve app to size a replica. |
319
352
  | `complete(model, messages, tools=None, max_new_tokens=2048, temperature=0.7, **kw)` | Async stream of `CompletionChunk` for a `CompletingModel`. |
@@ -342,7 +375,8 @@ Extending it:
342
375
  (there is an `encode_tool_call` for upstreams that report them structurally) —
343
376
  then `ServedCompletingModel` is its client, unchanged.
344
377
  - **A model no task's serve app can load** gets its own serve app — an `Image2Mesh`
345
- subclass, say — and, if its source is unusual too, its own importer.
378
+ subclass, say, with `load` and `make_mesh`, and `compile` if compiling it pays —
379
+ and, if its source is unusual too, its own importer.
346
380
 
347
381
  ## Notes
348
382
 
@@ -131,13 +131,14 @@ class AnthropicText2Text(HostedModel):
131
131
  def client(cls, url: str, name: str) -> ServedCompletingModel:
132
132
  return ServedCompletingModel(url=url, model_id=name)
133
133
 
134
- def __init__(self, family: str, suffix: str, run_name: str) -> None:
135
- config = cortexgrid.model_config(family, suffix, run_name)
134
+ def __init__(self, deployment: cortexgrid.DeploymentKey) -> None:
135
+ config = cortexgrid.model_config(deployment)
136
136
  missing = {MODEL_PARAM, API_KEY_SECRET_PARAM} - config.keys()
137
137
  if missing:
138
138
  raise RuntimeError(
139
- f"{family}/{suffix}/{run_name} is missing {sorted(missing)} from "
140
- "its config; set them on the model card"
139
+ f"{deployment.family}/{deployment.suffix}/{deployment.run_name} "
140
+ f"is missing {sorted(missing)} from its config; set them on the "
141
+ "model card"
141
142
  )
142
143
  self._model = config[MODEL_PARAM]
143
144
  self._client = AsyncAnthropic(
@@ -30,6 +30,12 @@ _GIB = float(1 << 30)
30
30
 
31
31
  ENABLE_THINKING_PARAM = "enable_thinking"
32
32
 
33
+ # Whether a replica runs its model through `torch.compile` (see
34
+ # `cortexgrid_infer.compiling`). Off unless the deployment's config says
35
+ # "true": compiled serving pays a warm-up per input shape, which is worth it
36
+ # only where traffic repeats those shapes.
37
+ COMPILE_PARAM = "compile"
38
+
33
39
  # Weights are only part of what a replica holds. The multiplier covers CUDA
34
40
  # context, activations and fragmentation; the floor covers the KV cache of a
35
41
  # long context, which does not scale with model size the way activations do.
@@ -98,7 +104,7 @@ class LocalModel:
98
104
 
99
105
  @classmethod
100
106
  def config(cls) -> dict[str, str]:
101
- return {}
107
+ return {COMPILE_PARAM: "false"}
102
108
 
103
109
  @classmethod
104
110
  def client(cls, url: str, name: str) -> DeployedModel:
@@ -36,13 +36,13 @@ class GeminiModel(HostedModel):
36
36
  holding the key to send it with."""
37
37
  return {MODEL_PARAM: model_id, API_KEY_SECRET_PARAM: api_key_secret}
38
38
 
39
- def __init__(self, family: str, suffix: str, run_name: str) -> None:
40
- config = cortexgrid.model_config(family, suffix, run_name)
39
+ def __init__(self, deployment: cortexgrid.DeploymentKey) -> None:
40
+ config = cortexgrid.model_config(deployment)
41
41
  missing = {MODEL_PARAM, API_KEY_SECRET_PARAM} - config.keys()
42
42
  if missing:
43
43
  raise RuntimeError(
44
- f"{family}/{suffix}/{run_name} is missing {sorted(missing)} from "
45
- "its config; set them on the model card"
44
+ f"{deployment.family}/{deployment.suffix}/{deployment.run_name} is "
45
+ f"missing {sorted(missing)} from its config; set them on the model card"
46
46
  )
47
47
  self._config = config
48
48
  self._model = config[MODEL_PARAM]
@@ -13,6 +13,7 @@ cortexgrid bundles the subclass's own file, so the model's code travels with it.
13
13
  min_vram_gb = 6.0
14
14
  def load(self, path, device): ...
15
15
  def make_mesh(self, image, **options) -> GeneratedMesh: ...
16
+ def compile(self, device): ... # optional; see `Image2Mesh.compile`
16
17
 
17
18
  imp = HuggingFaceImporter("org/my-mesh-model", MyMesh)
18
19
  """
@@ -31,11 +32,12 @@ from fastapi import FastAPI
31
32
  from PIL import Image, ImageOps
32
33
  import torch
33
34
 
35
+ from cortexgrid_infer import compiling
34
36
  from cortexgrid_infer.core import GeneratedMesh
35
37
  from cortexgrid_infer.device import detect_device
36
38
  from cortexgrid_infer.protocols import meshing
37
39
  from cortexgrid_infer.protocols.meshing import ServedMeshingModel
38
- from cortexgrid_infer.serve_apps.base import LocalModel
40
+ from cortexgrid_infer.serve_apps.base import COMPILE_PARAM, LocalModel
39
41
 
40
42
 
41
43
  _app = FastAPI()
@@ -52,14 +54,30 @@ class Image2Mesh(LocalModel):
52
54
  def client(cls, url: str, name: str) -> ServedMeshingModel:
53
55
  return ServedMeshingModel(url=url, model_id=name)
54
56
 
55
- def __init__(self, family: str, suffix: str, run_name: str) -> None:
57
+ def __init__(self, deployment: cortexgrid.DeploymentKey) -> None:
56
58
  self.device = detect_device()
57
- self.load(Path(cortexgrid.load_model(family, suffix, run_name)), self.device)
59
+ path = cortexgrid.load_model(deployment.family, deployment.suffix, deployment.run_name)
60
+ self.load(Path(path), self.device)
61
+ settings = cortexgrid.model_config(deployment)
62
+ requested = settings.get(COMPILE_PARAM, "false") == "true"
63
+ self.compiled = requested and compiling.supported(self.device)
64
+ if self.compiled:
65
+ self.compile(self.device)
58
66
 
59
67
  def load(self, path: Path, device: torch.device) -> None:
60
68
  """Build the model from its files at `path`, on `device`."""
61
69
  raise NotImplementedError
62
70
 
71
+ def compile(self, device: torch.device) -> None:
72
+ """Compile what `load` built, on `device`.
73
+
74
+ Called after `load`, and only when the model card asks for it on a
75
+ device worth compiling on. Does nothing unless overridden: which of a
76
+ model's modules pay back compiling, and in which `compiling.Mode`, is
77
+ the model's to know - a denoising loop of fixed shapes suits
78
+ `Mode.GRAPHED`, a decoder queried over a grid whose size moves suits
79
+ `Mode.FUSED`. `compiling.compile_module` does the rest."""
80
+
63
81
  def make_mesh(self, image: Image.Image, **options: Any) -> GeneratedMesh:
64
82
  """The mesh of the object in `image`, in the frame `GeneratedMesh`
65
83
  documents, with the options the request carried; `params` says what the
@@ -29,7 +29,7 @@ import torch
29
29
  from cortexgrid_infer import compiling
30
30
  from cortexgrid_infer.device import detect_device
31
31
  from cortexgrid_infer.protocols.imaging import ServedGeneratingModel
32
- from cortexgrid_infer.serve_apps.base import LocalModel
32
+ from cortexgrid_infer.serve_apps.base import COMPILE_PARAM, LocalModel
33
33
 
34
34
 
35
35
  _app = FastAPI()
@@ -84,8 +84,8 @@ class Text2Image(LocalModel):
84
84
  def client(cls, url: str, name: str) -> ServedGeneratingModel:
85
85
  return ServedGeneratingModel(url=url, model_id=name)
86
86
 
87
- def __init__(self, family: str, suffix: str, run_name: str) -> None:
88
- path = cortexgrid.load_model(family, suffix, run_name)
87
+ def __init__(self, deployment: cortexgrid.DeploymentKey) -> None:
88
+ path = cortexgrid.load_model(deployment.family, deployment.suffix, deployment.run_name)
89
89
  self._device = detect_device()
90
90
  pipe = _pipeline_class(path).from_pretrained(path, torch_dtype=torch.bfloat16)
91
91
  pipe = pipe.to(self._device)
@@ -95,8 +95,12 @@ class Text2Image(LocalModel):
95
95
  vae.enable_tiling()
96
96
  self._pipe = pipe
97
97
  # A denoising schedule is a loop of identically shaped forwards: capture once, replay.
98
- self._compiled = compiling.compile_pipeline(
99
- pipe, self._device, compiling.Mode.GRAPHED
98
+ settings = cortexgrid.model_config(deployment)
99
+ requested = settings.get(COMPILE_PARAM, "false") == "true"
100
+ self._compiled = (
101
+ compiling.compile_pipeline(pipe, self._device, compiling.Mode.GRAPHED)
102
+ if requested
103
+ else []
100
104
  )
101
105
 
102
106
  @_app.post("/generate")
@@ -30,7 +30,11 @@ import torch
30
30
  from cortexgrid_infer import compiling
31
31
  from cortexgrid_infer.device import detect_device
32
32
  from cortexgrid_infer.protocols.completion import ServedCompletingModel
33
- from cortexgrid_infer.serve_apps.base import ENABLE_THINKING_PARAM, LocalModel
33
+ from cortexgrid_infer.serve_apps.base import (
34
+ COMPILE_PARAM,
35
+ ENABLE_THINKING_PARAM,
36
+ LocalModel,
37
+ )
34
38
 
35
39
 
36
40
  log = logging.getLogger(__name__)
@@ -62,14 +66,16 @@ _RESERVED_BODY_KEYS = {
62
66
  class Text2Text(LocalModel):
63
67
  @classmethod
64
68
  def config(cls) -> dict[str, str]:
65
- return {ENABLE_THINKING_PARAM: "true"}
69
+ return {**super().config(), ENABLE_THINKING_PARAM: "true"}
66
70
 
67
71
  @classmethod
68
72
  def client(cls, url: str, name: str) -> ServedCompletingModel:
69
73
  return ServedCompletingModel(url=url, model_id=name)
70
74
 
71
- def __init__(self, family: str, suffix: str, run_name: str) -> None:
72
- path = cortexgrid.load_model(family, suffix, run_name)
75
+ def __init__(self, deployment: cortexgrid.DeploymentKey) -> None:
76
+ path = cortexgrid.load_model(
77
+ deployment.family, deployment.suffix, deployment.run_name
78
+ )
73
79
  self._device = detect_device()
74
80
  self._tokenizer = AutoTokenizer.from_pretrained(path)
75
81
  if self._tokenizer.pad_token is None:
@@ -78,7 +84,7 @@ class Text2Text(LocalModel):
78
84
  str(path), torch_dtype=torch.float16
79
85
  )
80
86
  self._model.to(self._device) # type: ignore
81
- settings = cortexgrid.model_config(family, suffix, run_name)
87
+ settings = cortexgrid.model_config(deployment)
82
88
  context = getattr(
83
89
  self._model.config, "max_position_embeddings", DEFAULT_MAX_TOTAL_TOKENS
84
90
  )
@@ -90,7 +96,8 @@ class Text2Text(LocalModel):
90
96
  # pins the shape every forward sees, so the loop is captured once
91
97
  # instead of recompiled per token. transformers then compiles `generate`
92
98
  # itself, which is why the module is not compiled here as well.
93
- self._compiled = compiling.supported(self._device)
99
+ requested = settings.get(COMPILE_PARAM, "false") == "true"
100
+ self._compiled = requested and compiling.supported(self._device)
94
101
  if self._compiled:
95
102
  config = self._model.generation_config
96
103
  config.cache_implementation = "static"
@@ -0,0 +1,107 @@
1
+ """The text-rewriting serve app: an encoder-decoder LM loaded from the cortexgrid
2
+ registry, answering `POST /rewrite` (see `cortexgrid_infer.protocols.rewriting`).
3
+
4
+ Loads with `AutoModelForSeq2SeqLM`, so it runs any encoder-decoder model (T5,
5
+ BART, Marian, ...) whose weights are in the transformers layout, whichever
6
+ importer staged them. Any task prefix the model expects (`gec: ` for grammar
7
+ correction, say) is the caller's to put in the text.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Any
14
+
15
+ import cortexgrid
16
+ from cortexgrid import serve
17
+ from fastapi import FastAPI
18
+ from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, CompileConfig
19
+ import torch
20
+
21
+ from cortexgrid_infer import compiling
22
+ from cortexgrid_infer.device import detect_device
23
+ from cortexgrid_infer.protocols.rewriting import ServedRewritingModel
24
+ from cortexgrid_infer.serve_apps.base import COMPILE_PARAM, LocalModel
25
+
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+ _app = FastAPI()
30
+
31
+ # Output budget for a request that names none: T5's training context, and
32
+ # generation stops at the end-of-sequence token well before it for most text.
33
+ DEFAULT_MAX_NEW_TOKENS = 512
34
+
35
+ # Lengths a compiled replica pads a text up to. `generate` sizes the
36
+ # cross-attention cache to the encoded text, so every distinct length would
37
+ # capture the decode loop afresh; rounding up to a power of two bounds that at
38
+ # one capture per bucket, at the cost of a little masked-out padding.
39
+ _INPUT_BUCKETS = (32, 64, 128, 256, 512)
40
+
41
+
42
+ def _bucket(length: int) -> int:
43
+ """The smallest of `_INPUT_BUCKETS` that holds `length` tokens, else the largest."""
44
+ return next((b for b in _INPUT_BUCKETS if b >= length), _INPUT_BUCKETS[-1])
45
+
46
+
47
+ @serve.ingress(_app)
48
+ class TextRewriter(LocalModel):
49
+ @classmethod
50
+ def client(cls, url: str, name: str) -> ServedRewritingModel:
51
+ return ServedRewritingModel(url=url, model_id=name)
52
+
53
+ def __init__(self, deployment: cortexgrid.DeploymentKey) -> None:
54
+ path = cortexgrid.load_model(deployment.family, deployment.suffix, deployment.run_name)
55
+ self._device = detect_device()
56
+ self._tokenizer = AutoTokenizer.from_pretrained(path)
57
+ self._model = AutoModelForSeq2SeqLM.from_pretrained(
58
+ str(path), torch_dtype=torch.bfloat16
59
+ )
60
+ self._model.to(self._device)
61
+ settings = cortexgrid.model_config(deployment)
62
+ # As in Text2Text: a static cache pins the decoder's self-attention to
63
+ # one shape, and transformers compiles the decode step itself. The
64
+ # cross-attention cache is pinned by padding the text; see `_tokenize`.
65
+ requested = settings.get(COMPILE_PARAM, "false") == "true"
66
+ self._compiled = requested and compiling.supported(self._device)
67
+ if self._compiled:
68
+ config = self._model.generation_config
69
+ config.cache_implementation = "static"
70
+ config.max_cache_len = DEFAULT_MAX_NEW_TOKENS
71
+ config.compile_config = CompileConfig(mode=compiling.Mode.GRAPHED)
72
+
73
+ def _tokenize(self, text: str) -> Any:
74
+ """The model's inputs for `text`, on the device.
75
+
76
+ Compiled, the text is padded up to its bucket, and one longer than the
77
+ last bucket is cut to it, keeping its start."""
78
+ if not self._compiled:
79
+ return self._tokenizer(text, return_tensors="pt").to(self._device)
80
+ length = len(self._tokenizer(text)["input_ids"])
81
+ bucket = _bucket(length)
82
+ if length > bucket:
83
+ log.warning(
84
+ "truncating text from %d to %d tokens, the most this replica takes",
85
+ length, bucket,
86
+ )
87
+ return self._tokenizer(
88
+ text,
89
+ padding="max_length",
90
+ truncation=True,
91
+ max_length=bucket,
92
+ return_tensors="pt",
93
+ ).to(self._device)
94
+
95
+ @_app.post("/rewrite")
96
+ async def rewrite(self, body: dict[str, Any]) -> dict[str, str]:
97
+ generation_options = {"max_new_tokens": DEFAULT_MAX_NEW_TOKENS} | {
98
+ key: value for key, value in body.items() if key != "text"
99
+ }
100
+ if self._compiled:
101
+ # A reply longer than the cache would reallocate it and recompile.
102
+ generation_options["max_new_tokens"] = min(
103
+ generation_options["max_new_tokens"], DEFAULT_MAX_NEW_TOKENS
104
+ )
105
+ inputs = self._tokenize(body["text"])
106
+ output = self._model.generate(**inputs, **generation_options)
107
+ return {"text": self._tokenizer.decode(output[0], skip_special_tokens=True)}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cortexgrid-infer"
3
- version = "0.3.4"
3
+ version = "0.3.6"
4
4
  description = "Serve apps per task (text-to-text, text-to-image, image-to-mesh) and importers per source (HuggingFace) for running models on cortexgrid"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -15,7 +15,9 @@ dependencies = [
15
15
  # 0.3.4: jobs return results (remote -> JobFuture), register_model for a
16
16
  # model that stages no weights, model_config, and deploy_model skipping a
17
17
  # redundant PUT - which is why this library has no reuse guard of its own.
18
- "cortexgrid>=0.3.4",
18
+ # 0.3.12: DeploymentKey - a serve-app is constructed with its deployment's
19
+ # key and reads that deployment's config with model_config(key).
20
+ "cortexgrid>=0.3.12",
19
21
  "torch>=2.0.0",
20
22
  # 4.47: CompileConfig, which the completion serve app hands to `generate`
21
23
  # alongside a static cache so transformers compiles the decode step itself.
@@ -1,54 +0,0 @@
1
- """The text-rewriting serve app: an encoder-decoder LM loaded from the cortexgrid
2
- registry, answering `POST /rewrite` (see `cortexgrid_infer.protocols.rewriting`).
3
-
4
- Loads with `AutoModelForSeq2SeqLM`, so it runs any encoder-decoder model (T5,
5
- BART, Marian, ...) whose weights are in the transformers layout, whichever
6
- importer staged them. Any task prefix the model expects (`gec: ` for grammar
7
- correction, say) is the caller's to put in the text.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from typing import Any
13
-
14
- import cortexgrid
15
- from cortexgrid import serve
16
- from fastapi import FastAPI
17
- from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
18
- import torch
19
-
20
- from cortexgrid_infer.device import detect_device
21
- from cortexgrid_infer.protocols.rewriting import ServedRewritingModel
22
- from cortexgrid_infer.serve_apps.base import LocalModel
23
-
24
-
25
- _app = FastAPI()
26
-
27
- # Output budget for a request that names none: T5's training context, and
28
- # generation stops at the end-of-sequence token well before it for most text.
29
- DEFAULT_MAX_NEW_TOKENS = 512
30
-
31
-
32
- @serve.ingress(_app)
33
- class TextRewriter(LocalModel):
34
- @classmethod
35
- def client(cls, url: str, name: str) -> ServedRewritingModel:
36
- return ServedRewritingModel(url=url, model_id=name)
37
-
38
- def __init__(self, family: str, suffix: str, run_name: str) -> None:
39
- path = cortexgrid.load_model(family, suffix, run_name)
40
- self._device = detect_device()
41
- self._tokenizer = AutoTokenizer.from_pretrained(path)
42
- self._model = AutoModelForSeq2SeqLM.from_pretrained(
43
- str(path), torch_dtype=torch.bfloat16
44
- )
45
- self._model.to(self._device)
46
-
47
- @_app.post("/rewrite")
48
- async def rewrite(self, body: dict[str, Any]) -> dict[str, str]:
49
- generation_options = {"max_new_tokens": DEFAULT_MAX_NEW_TOKENS} | {
50
- key: value for key, value in body.items() if key != "text"
51
- }
52
- inputs = self._tokenizer(body["text"], return_tensors="pt").to(self._device)
53
- output = self._model.generate(**inputs, **generation_options)
54
- return {"text": self._tokenizer.decode(output[0], skip_special_tokens=True)}