cortexgrid-infer 0.3.6__tar.gz → 0.3.8__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.3.6 → cortexgrid_infer-0.3.8}/PKG-INFO +23 -24
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/README.md +21 -22
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/__init__.py +2 -4
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/core.py +26 -10
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/importers/base.py +0 -4
- cortexgrid_infer-0.3.8/cortexgrid_infer/protocols/completion.py +83 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/protocols/imaging.py +0 -10
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/protocols/meshing.py +0 -10
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/protocols/rewriting.py +0 -9
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/registry.py +5 -13
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/anthropic.py +22 -10
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/base.py +5 -7
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/gemini/text2image.py +3 -2
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/gemini/text2text.py +27 -15
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/image2mesh.py +2 -2
- cortexgrid_infer-0.3.8/cortexgrid_infer/serve_apps/streamed_completion.py +165 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/text2image.py +2 -2
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/text2text.py +145 -8
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/text_rewriter.py +2 -2
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/pyproject.toml +2 -2
- cortexgrid_infer-0.3.6/cortexgrid_infer/protocols/completion.py +0 -222
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/.gitignore +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/LICENSE +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/compiling.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/device.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/importers/__init__.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/importers/huggingface.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/protocols/__init__.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/py.typed +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/__init__.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/gemini/__init__.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/cortexgrid_infer/serve_apps/gemini/base.py +0 -0
- {cortexgrid_infer-0.3.6 → cortexgrid_infer-0.3.8}/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.
|
|
3
|
+
Version: 0.3.8
|
|
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.
|
|
12
|
+
Requires-Dist: cortexgrid>=0.3.16
|
|
13
13
|
Requires-Dist: diffusers>=0.38
|
|
14
14
|
Requires-Dist: fastapi>=0.133
|
|
15
15
|
Requires-Dist: google-genai>=1.0
|
|
@@ -85,11 +85,12 @@ need:
|
|
|
85
85
|
| `.serve_app` | the class cortexgrid bundles and runs on the cluster |
|
|
86
86
|
| `.requirements()` | what one replica needs to be placed and to run |
|
|
87
87
|
| `.config()` | settings the serve app reads at construction, if it needs any |
|
|
88
|
-
| `.client(url)` | a client that speaks the deployed app's routes |
|
|
89
88
|
| `.source` | *(importers only)* a callable that downloads the weights and returns the directory |
|
|
90
89
|
|
|
91
90
|
One with weights goes through `cortexgrid.import_model`, one without through
|
|
92
|
-
`cortexgrid.register_model`.
|
|
91
|
+
`cortexgrid.register_model`. Once deployed, the `cortexgrid.Deployment` hands back
|
|
92
|
+
the client that speaks the app's routes: `deployment.client()` once the app serves,
|
|
93
|
+
or `deployment.client_async()` at once, with `await model.is_ready()` to ask.
|
|
93
94
|
|
|
94
95
|
## Quick start
|
|
95
96
|
|
|
@@ -117,13 +118,11 @@ cortexgrid.remote(
|
|
|
117
118
|
import_weights, imp, imp.requirements(), num_gpus=0, num_cpus=2
|
|
118
119
|
).result()
|
|
119
120
|
|
|
120
|
-
# 2. Deploy
|
|
121
|
-
deployment = cortexgrid.deploy_model(
|
|
122
|
-
imp.family, imp.suffix, cortexgrid.IMPORTED, wait=True, timeout=1800
|
|
123
|
-
)
|
|
121
|
+
# 2. Deploy.
|
|
122
|
+
deployment = cortexgrid.deploy_model(imp.family, imp.suffix, cortexgrid.IMPORTED, timeout=1800)
|
|
124
123
|
|
|
125
|
-
# 3. Inference.
|
|
126
|
-
model =
|
|
124
|
+
# 3. Inference — the client blocks until the Ray Serve app is running.
|
|
125
|
+
model = deployment.client()
|
|
127
126
|
|
|
128
127
|
async def run():
|
|
129
128
|
result = await mg.generate(model, "a red bicycle on a beach at sunrise")
|
|
@@ -281,8 +280,7 @@ it is the floor; only quantizing the weights or running fewer forwards moves it.
|
|
|
281
280
|
`complete` streams `CompletionChunk`s (`.content`, `.thinking`, `.tool_calls`, `.finish_reason`):
|
|
282
281
|
|
|
283
282
|
```python
|
|
284
|
-
|
|
285
|
-
model = imp.client(deployment.url)
|
|
283
|
+
model = deployment.client()
|
|
286
284
|
messages = [{"role": "user", "content": "Explain RAG in one sentence."}]
|
|
287
285
|
|
|
288
286
|
async for chunk in mg.complete(model, messages, max_new_tokens=512, temperature=0.7):
|
|
@@ -323,7 +321,7 @@ a re-deploy rather than a re-registration.
|
|
|
323
321
|
|
|
324
322
|
A replica asks for no hardware at all, so it is placed on any node, CPU-only
|
|
325
323
|
included. From there it deploys and streams exactly like a cluster-served model:
|
|
326
|
-
`
|
|
324
|
+
`deployment.client()` returns the same `ServedCompletingModel`. Anthropic
|
|
327
325
|
reports tool calls as structured blocks rather than as generated text, so the
|
|
328
326
|
serve app re-encodes them into the text form the client parses.
|
|
329
327
|
|
|
@@ -346,7 +344,7 @@ cortexgrid.register_model(
|
|
|
346
344
|
|
|
347
345
|
Everything said above for Anthropic holds: `config` carries the model name and
|
|
348
346
|
the name of the secret (`api_key_secret="..."` to use another), both editable on
|
|
349
|
-
the model card; a replica needs no hardware; and `
|
|
347
|
+
the model card; a replica needs no hardware; and `deployment.client()`
|
|
350
348
|
is a `ServedCompletingModel`, with Gemini's function calls re-encoded into the
|
|
351
349
|
text form the client parses.
|
|
352
350
|
|
|
@@ -356,7 +354,8 @@ client is a `ServedGeneratingModel`:
|
|
|
356
354
|
```python
|
|
357
355
|
entry = mg.Hosted("gemini-2.5-flash-image", mg.GeminiText2Image)
|
|
358
356
|
...
|
|
359
|
-
|
|
357
|
+
model = deployment.client()
|
|
358
|
+
picture = await mg.generate(model, "a fox in the snow", size=2048)
|
|
360
359
|
```
|
|
361
360
|
|
|
362
361
|
It takes a prompt only, and makes a square picture. `size` picks Gemini's size
|
|
@@ -368,28 +367,28 @@ diffusion settings with no Gemini counterpart, and come back as `None`.
|
|
|
368
367
|
| | |
|
|
369
368
|
|---|---|
|
|
370
369
|
| `HuggingFaceImporter(hf_id, serve_app, token=None, ignore_patterns=None)` | Importer for a model on the HuggingFace Hub, run by `serve_app`. |
|
|
371
|
-
| `Importer` | Base of the importers: identity, scratch directory, `source`, and `requirements()`
|
|
370
|
+
| `Importer` | Base of the importers: identity, scratch directory, `source`, and `requirements()` taken from the serve app. A new source implements `download(local_dir)` and `weights()`. |
|
|
372
371
|
| `Hosted(model_id, serve_app, **settings)` | Entry for a model hosted elsewhere; no weights. `settings` are the serve app's `config` arguments. |
|
|
373
|
-
| `ModelEntry` | Base of both: `model_id`, `family`, `suffix`, `serve_app`, `requirements()`, `config()
|
|
372
|
+
| `ModelEntry` | Base of both: `model_id`, `family`, `suffix`, `serve_app`, `requirements()`, `config()`. |
|
|
374
373
|
| `Text2Text`, `Text2Image`, `TextRewriter`, `Image2Mesh` | Serve apps that run weights (`LocalModel`s). |
|
|
375
374
|
| `AnthropicText2Text` | Serve app that forwards to the Anthropic API (a `HostedModel`). |
|
|
376
375
|
| `GeminiText2Text`, `GeminiText2Image` | Serve apps that forward to the Gemini API (`HostedModel`s sharing the `GeminiModel` base). |
|
|
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(
|
|
378
|
-
| `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`, `client(
|
|
376
|
+
| `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(deployment)`. |
|
|
377
|
+
| `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`, `client(deployment)`. |
|
|
379
378
|
| `Weights(params=None, file_bytes=0)` | What an importer reads about the weights, handed to the serve app to size a replica. |
|
|
380
379
|
| `complete(model, messages, tools=None, max_new_tokens=2048, temperature=0.7, **kw)` | Async stream of `CompletionChunk` for a `CompletingModel`. |
|
|
381
380
|
| `generate(model, prompt, *, image=None, **kw) -> GeneratedImage` | One image from a `GeneratingModel`. |
|
|
382
|
-
| `ServedCompletingModel(
|
|
383
|
-
| `ServedGeneratingModel(
|
|
384
|
-
| `ServedMeshingModel(
|
|
381
|
+
| `ServedCompletingModel(key, url)` | Client for any deployed completion app. |
|
|
382
|
+
| `ServedGeneratingModel(key, url)` | Client for any deployed text-to-image app. |
|
|
383
|
+
| `ServedMeshingModel(key, url)` | Client for any deployed image-to-mesh app. |
|
|
385
384
|
| `split_model_id(model_id) -> (family, suffix)` | The registry identity an importer derives. |
|
|
386
385
|
| `detect_device()` | The torch device a serve app should load onto. |
|
|
387
386
|
| `compiling.Mode.GRAPHED` / `compiling.Mode.FUSED` | The two modes a serve app chooses between. See [Compilation](#compilation). |
|
|
388
387
|
| `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. |
|
|
389
388
|
| `ModelDeployFailed` | Re-exported from cortexgrid; subclasses `RuntimeError`. |
|
|
390
389
|
|
|
391
|
-
Types: `
|
|
392
|
-
`CompletionChunk`, `GeneratedImage`, `GeneratedMesh`, `ToolCall`, `Message`, `Tool`,
|
|
390
|
+
Types: `CompletingModel`, `GeneratingModel`, `MeshingModel`, `RewritingModel` (each
|
|
391
|
+
a `cortexgrid.DeploymentClient`), `CompletionChunk`, `GeneratedImage`, `GeneratedMesh`, `ToolCall`, `Message`, `Tool`,
|
|
393
392
|
`ToolSpec`.
|
|
394
393
|
|
|
395
394
|
Extending it:
|
|
@@ -57,11 +57,12 @@ need:
|
|
|
57
57
|
| `.serve_app` | the class cortexgrid bundles and runs on the cluster |
|
|
58
58
|
| `.requirements()` | what one replica needs to be placed and to run |
|
|
59
59
|
| `.config()` | settings the serve app reads at construction, if it needs any |
|
|
60
|
-
| `.client(url)` | a client that speaks the deployed app's routes |
|
|
61
60
|
| `.source` | *(importers only)* a callable that downloads the weights and returns the directory |
|
|
62
61
|
|
|
63
62
|
One with weights goes through `cortexgrid.import_model`, one without through
|
|
64
|
-
`cortexgrid.register_model`.
|
|
63
|
+
`cortexgrid.register_model`. Once deployed, the `cortexgrid.Deployment` hands back
|
|
64
|
+
the client that speaks the app's routes: `deployment.client()` once the app serves,
|
|
65
|
+
or `deployment.client_async()` at once, with `await model.is_ready()` to ask.
|
|
65
66
|
|
|
66
67
|
## Quick start
|
|
67
68
|
|
|
@@ -89,13 +90,11 @@ cortexgrid.remote(
|
|
|
89
90
|
import_weights, imp, imp.requirements(), num_gpus=0, num_cpus=2
|
|
90
91
|
).result()
|
|
91
92
|
|
|
92
|
-
# 2. Deploy
|
|
93
|
-
deployment = cortexgrid.deploy_model(
|
|
94
|
-
imp.family, imp.suffix, cortexgrid.IMPORTED, wait=True, timeout=1800
|
|
95
|
-
)
|
|
93
|
+
# 2. Deploy.
|
|
94
|
+
deployment = cortexgrid.deploy_model(imp.family, imp.suffix, cortexgrid.IMPORTED, timeout=1800)
|
|
96
95
|
|
|
97
|
-
# 3. Inference.
|
|
98
|
-
model =
|
|
96
|
+
# 3. Inference — the client blocks until the Ray Serve app is running.
|
|
97
|
+
model = deployment.client()
|
|
99
98
|
|
|
100
99
|
async def run():
|
|
101
100
|
result = await mg.generate(model, "a red bicycle on a beach at sunrise")
|
|
@@ -253,8 +252,7 @@ it is the floor; only quantizing the weights or running fewer forwards moves it.
|
|
|
253
252
|
`complete` streams `CompletionChunk`s (`.content`, `.thinking`, `.tool_calls`, `.finish_reason`):
|
|
254
253
|
|
|
255
254
|
```python
|
|
256
|
-
|
|
257
|
-
model = imp.client(deployment.url)
|
|
255
|
+
model = deployment.client()
|
|
258
256
|
messages = [{"role": "user", "content": "Explain RAG in one sentence."}]
|
|
259
257
|
|
|
260
258
|
async for chunk in mg.complete(model, messages, max_new_tokens=512, temperature=0.7):
|
|
@@ -295,7 +293,7 @@ a re-deploy rather than a re-registration.
|
|
|
295
293
|
|
|
296
294
|
A replica asks for no hardware at all, so it is placed on any node, CPU-only
|
|
297
295
|
included. From there it deploys and streams exactly like a cluster-served model:
|
|
298
|
-
`
|
|
296
|
+
`deployment.client()` returns the same `ServedCompletingModel`. Anthropic
|
|
299
297
|
reports tool calls as structured blocks rather than as generated text, so the
|
|
300
298
|
serve app re-encodes them into the text form the client parses.
|
|
301
299
|
|
|
@@ -318,7 +316,7 @@ cortexgrid.register_model(
|
|
|
318
316
|
|
|
319
317
|
Everything said above for Anthropic holds: `config` carries the model name and
|
|
320
318
|
the name of the secret (`api_key_secret="..."` to use another), both editable on
|
|
321
|
-
the model card; a replica needs no hardware; and `
|
|
319
|
+
the model card; a replica needs no hardware; and `deployment.client()`
|
|
322
320
|
is a `ServedCompletingModel`, with Gemini's function calls re-encoded into the
|
|
323
321
|
text form the client parses.
|
|
324
322
|
|
|
@@ -328,7 +326,8 @@ client is a `ServedGeneratingModel`:
|
|
|
328
326
|
```python
|
|
329
327
|
entry = mg.Hosted("gemini-2.5-flash-image", mg.GeminiText2Image)
|
|
330
328
|
...
|
|
331
|
-
|
|
329
|
+
model = deployment.client()
|
|
330
|
+
picture = await mg.generate(model, "a fox in the snow", size=2048)
|
|
332
331
|
```
|
|
333
332
|
|
|
334
333
|
It takes a prompt only, and makes a square picture. `size` picks Gemini's size
|
|
@@ -340,28 +339,28 @@ diffusion settings with no Gemini counterpart, and come back as `None`.
|
|
|
340
339
|
| | |
|
|
341
340
|
|---|---|
|
|
342
341
|
| `HuggingFaceImporter(hf_id, serve_app, token=None, ignore_patterns=None)` | Importer for a model on the HuggingFace Hub, run by `serve_app`. |
|
|
343
|
-
| `Importer` | Base of the importers: identity, scratch directory, `source`, and `requirements()`
|
|
342
|
+
| `Importer` | Base of the importers: identity, scratch directory, `source`, and `requirements()` taken from the serve app. A new source implements `download(local_dir)` and `weights()`. |
|
|
344
343
|
| `Hosted(model_id, serve_app, **settings)` | Entry for a model hosted elsewhere; no weights. `settings` are the serve app's `config` arguments. |
|
|
345
|
-
| `ModelEntry` | Base of both: `model_id`, `family`, `suffix`, `serve_app`, `requirements()`, `config()
|
|
344
|
+
| `ModelEntry` | Base of both: `model_id`, `family`, `suffix`, `serve_app`, `requirements()`, `config()`. |
|
|
346
345
|
| `Text2Text`, `Text2Image`, `TextRewriter`, `Image2Mesh` | Serve apps that run weights (`LocalModel`s). |
|
|
347
346
|
| `AnthropicText2Text` | Serve app that forwards to the Anthropic API (a `HostedModel`). |
|
|
348
347
|
| `GeminiText2Text`, `GeminiText2Image` | Serve apps that forward to the Gemini API (`HostedModel`s sharing the `GeminiModel` base). |
|
|
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(
|
|
350
|
-
| `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`, `client(
|
|
348
|
+
| `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(deployment)`. |
|
|
349
|
+
| `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`, `client(deployment)`. |
|
|
351
350
|
| `Weights(params=None, file_bytes=0)` | What an importer reads about the weights, handed to the serve app to size a replica. |
|
|
352
351
|
| `complete(model, messages, tools=None, max_new_tokens=2048, temperature=0.7, **kw)` | Async stream of `CompletionChunk` for a `CompletingModel`. |
|
|
353
352
|
| `generate(model, prompt, *, image=None, **kw) -> GeneratedImage` | One image from a `GeneratingModel`. |
|
|
354
|
-
| `ServedCompletingModel(
|
|
355
|
-
| `ServedGeneratingModel(
|
|
356
|
-
| `ServedMeshingModel(
|
|
353
|
+
| `ServedCompletingModel(key, url)` | Client for any deployed completion app. |
|
|
354
|
+
| `ServedGeneratingModel(key, url)` | Client for any deployed text-to-image app. |
|
|
355
|
+
| `ServedMeshingModel(key, url)` | Client for any deployed image-to-mesh app. |
|
|
357
356
|
| `split_model_id(model_id) -> (family, suffix)` | The registry identity an importer derives. |
|
|
358
357
|
| `detect_device()` | The torch device a serve app should load onto. |
|
|
359
358
|
| `compiling.Mode.GRAPHED` / `compiling.Mode.FUSED` | The two modes a serve app chooses between. See [Compilation](#compilation). |
|
|
360
359
|
| `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. |
|
|
361
360
|
| `ModelDeployFailed` | Re-exported from cortexgrid; subclasses `RuntimeError`. |
|
|
362
361
|
|
|
363
|
-
Types: `
|
|
364
|
-
`CompletionChunk`, `GeneratedImage`, `GeneratedMesh`, `ToolCall`, `Message`, `Tool`,
|
|
362
|
+
Types: `CompletingModel`, `GeneratingModel`, `MeshingModel`, `RewritingModel` (each
|
|
363
|
+
a `cortexgrid.DeploymentClient`), `CompletionChunk`, `GeneratedImage`, `GeneratedMesh`, `ToolCall`, `Message`, `Tool`,
|
|
365
364
|
`ToolSpec`.
|
|
366
365
|
|
|
367
366
|
Extending it:
|
|
@@ -18,8 +18,8 @@ nothing else, split along the two things a model is:
|
|
|
18
18
|
family=imp.family, suffix=imp.suffix,
|
|
19
19
|
requirements=imp.requirements(),
|
|
20
20
|
)
|
|
21
|
-
deployment = cortexgrid.deploy_model(imp.family, imp.suffix, cortexgrid.IMPORTED
|
|
22
|
-
model =
|
|
21
|
+
deployment = cortexgrid.deploy_model(imp.family, imp.suffix, cortexgrid.IMPORTED)
|
|
22
|
+
model = deployment.client()
|
|
23
23
|
|
|
24
24
|
A model hosted elsewhere has no weights and so no importer: its serve app
|
|
25
25
|
forwards to it, and a `Hosted` entry registers it.
|
|
@@ -31,7 +31,6 @@ from cortexgrid import ModelDeployFailed
|
|
|
31
31
|
|
|
32
32
|
from cortexgrid_infer.core import (
|
|
33
33
|
CompletionChunk,
|
|
34
|
-
DeployedModel,
|
|
35
34
|
CompletingModel,
|
|
36
35
|
GeneratedImage,
|
|
37
36
|
GeneratedMesh,
|
|
@@ -71,7 +70,6 @@ from cortexgrid_infer.importers import HuggingFaceImporter, Importer
|
|
|
71
70
|
|
|
72
71
|
__all__ = [
|
|
73
72
|
"CompletionChunk",
|
|
74
|
-
"DeployedModel",
|
|
75
73
|
"CompletingModel",
|
|
76
74
|
"GeneratedImage",
|
|
77
75
|
"GeneratedMesh",
|
|
@@ -8,6 +8,7 @@ from functools import partial
|
|
|
8
8
|
from collections.abc import AsyncIterator
|
|
9
9
|
from typing import Any, Callable, Sequence
|
|
10
10
|
|
|
11
|
+
import cortexgrid
|
|
11
12
|
import numpy as np
|
|
12
13
|
|
|
13
14
|
|
|
@@ -37,19 +38,34 @@ class CompletionChunk:
|
|
|
37
38
|
def has_tool_calls(self) -> bool:
|
|
38
39
|
return bool(self.tool_calls)
|
|
39
40
|
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_wire(
|
|
43
|
+
cls, answered: dict[str, Any], tools_by_name: dict[str, Callable[..., Any]]
|
|
44
|
+
) -> CompletionChunk:
|
|
45
|
+
tool_calls = []
|
|
46
|
+
for called in answered["tool_calls"]:
|
|
47
|
+
tool = tools_by_name.get(called["name"])
|
|
48
|
+
if tool is None:
|
|
49
|
+
continue
|
|
50
|
+
bound = partial(tool, **called["arguments"])
|
|
51
|
+
tool_call = ToolCall(
|
|
52
|
+
id=called["id"], name=called["name"], arguments=called["arguments"], _func=bound
|
|
53
|
+
)
|
|
54
|
+
tool_calls.append(tool_call)
|
|
55
|
+
return cls(
|
|
56
|
+
content=answered["content"],
|
|
57
|
+
thinking=answered["thinking"],
|
|
58
|
+
tool_calls=tool_calls,
|
|
59
|
+
finish_reason=answered["finish_reason"],
|
|
60
|
+
)
|
|
61
|
+
|
|
40
62
|
|
|
41
63
|
Message = dict[str, Any]
|
|
42
64
|
ToolSpec = dict[str, Any]
|
|
43
65
|
Tool = Callable[..., Any] | ToolSpec
|
|
44
66
|
|
|
45
67
|
|
|
46
|
-
class
|
|
47
|
-
@property
|
|
48
|
-
@abc.abstractmethod
|
|
49
|
-
def name(self) -> str: ...
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class CompletingModel(DeployedModel):
|
|
68
|
+
class CompletingModel(cortexgrid.DeploymentClient, abc.ABC):
|
|
53
69
|
@abc.abstractmethod
|
|
54
70
|
def complete(
|
|
55
71
|
self,
|
|
@@ -72,7 +88,7 @@ class GeneratedImage:
|
|
|
72
88
|
params: dict[str, Any] = field(default_factory=dict)
|
|
73
89
|
|
|
74
90
|
|
|
75
|
-
class GeneratingModel(
|
|
91
|
+
class GeneratingModel(cortexgrid.DeploymentClient, abc.ABC):
|
|
76
92
|
"""A deployed image model. Unlike completion, generation is a single
|
|
77
93
|
request/response (no token streaming), so `generate` returns one result."""
|
|
78
94
|
|
|
@@ -103,14 +119,14 @@ class GeneratedMesh:
|
|
|
103
119
|
params: dict[str, Any] = field(default_factory=dict)
|
|
104
120
|
|
|
105
121
|
|
|
106
|
-
class MeshingModel(
|
|
122
|
+
class MeshingModel(cortexgrid.DeploymentClient, abc.ABC):
|
|
107
123
|
"""A deployed model that makes a mesh of the object in one picture."""
|
|
108
124
|
|
|
109
125
|
@abc.abstractmethod
|
|
110
126
|
async def mesh(self, image: bytes, **kwargs: Any) -> GeneratedMesh: ...
|
|
111
127
|
|
|
112
128
|
|
|
113
|
-
class RewritingModel(
|
|
129
|
+
class RewritingModel(cortexgrid.DeploymentClient, abc.ABC):
|
|
114
130
|
"""A deployed model that rewrites one text into another - corrects,
|
|
115
131
|
paraphrases, summarises or translates it. Like generation, a single
|
|
116
132
|
request/response."""
|
|
@@ -29,7 +29,6 @@ from typing import Self
|
|
|
29
29
|
|
|
30
30
|
import cortexgrid
|
|
31
31
|
|
|
32
|
-
from cortexgrid_infer.core import DeployedModel
|
|
33
32
|
from cortexgrid_infer.registry import ModelEntry, split_model_id
|
|
34
33
|
from cortexgrid_infer.serve_apps.base import LocalModel, Weights
|
|
35
34
|
|
|
@@ -97,6 +96,3 @@ class Importer(ModelEntry):
|
|
|
97
96
|
|
|
98
97
|
def config(self) -> dict[str, str]:
|
|
99
98
|
return self.serve_app.config()
|
|
100
|
-
|
|
101
|
-
def client(self, url: str) -> DeployedModel:
|
|
102
|
-
return self.serve_app.client(url, self.model_id)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""The wire protocol between a cortexgrid-served completion app and its client.
|
|
2
|
+
|
|
3
|
+
A completion serve app streams plain text from `POST /complete`, with tool calls
|
|
4
|
+
inlined in the text as `<tool_call>{"name": ..., "arguments": {...}}</tool_call>`.
|
|
5
|
+
That is the format an instruct-tuned causal LM emits on its own, so a serve app
|
|
6
|
+
wrapping one forwards its tokens untouched; a serve app wrapping a provider with
|
|
7
|
+
structured tool calls encodes them into the same form with `encode_tool_call`.
|
|
8
|
+
|
|
9
|
+
Either way the client is the same `ServedCompletingModel`, which streams text
|
|
10
|
+
back as `CompletionChunk`s and reassembles the tool calls.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import AsyncIterator
|
|
16
|
+
import json
|
|
17
|
+
from typing import Any, Sequence
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
from cortexgrid_infer.core import CompletingModel, CompletionChunk, Message, Tool
|
|
22
|
+
from cortexgrid_infer.utils import build_tool_map, normalize_tools
|
|
23
|
+
|
|
24
|
+
class ServedCompletingModel(CompletingModel):
|
|
25
|
+
"""Client for a completion app cortexgrid has deployed at `url`.
|
|
26
|
+
|
|
27
|
+
Provider-agnostic: every completion serve app speaks the protocol above, so
|
|
28
|
+
which model is behind it only shows up in its deployment's `key`."""
|
|
29
|
+
|
|
30
|
+
async def loglikelihoods(
|
|
31
|
+
self, messages: list[Message], continuations: list[str]
|
|
32
|
+
) -> list[float]:
|
|
33
|
+
body = {"messages": messages, "continuations": continuations}
|
|
34
|
+
loglikelihoods_url = f"{self.url}/loglikelihoods"
|
|
35
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
36
|
+
response = await client.post(loglikelihoods_url, json=body)
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
answered = response.json()
|
|
39
|
+
return answered["loglikelihoods"]
|
|
40
|
+
|
|
41
|
+
async def complete(
|
|
42
|
+
self,
|
|
43
|
+
messages: list[Message],
|
|
44
|
+
tools: Sequence[Tool] | None = None,
|
|
45
|
+
max_new_tokens: int = 16 * 1024,
|
|
46
|
+
temperature: float = 0.7,
|
|
47
|
+
top_p: float = 0.9,
|
|
48
|
+
top_k: int = 50,
|
|
49
|
+
repetition_penalty: float = 1.0,
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
) -> AsyncIterator[CompletionChunk]:
|
|
52
|
+
tool_specs = normalize_tools(tools)
|
|
53
|
+
tools_by_name = build_tool_map(tools)
|
|
54
|
+
body: dict[str, Any] = {
|
|
55
|
+
"messages": messages,
|
|
56
|
+
"tools": tool_specs,
|
|
57
|
+
"max_new_tokens": max_new_tokens,
|
|
58
|
+
"temperature": temperature,
|
|
59
|
+
"top_p": top_p,
|
|
60
|
+
"top_k": top_k,
|
|
61
|
+
"repetition_penalty": repetition_penalty,
|
|
62
|
+
**kwargs,
|
|
63
|
+
}
|
|
64
|
+
complete_url = f"{self.url}/complete"
|
|
65
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
66
|
+
async with client.stream("POST", complete_url, json=body) as response:
|
|
67
|
+
response.raise_for_status()
|
|
68
|
+
async for line in response.aiter_lines():
|
|
69
|
+
answered = json.loads(line)
|
|
70
|
+
chunk = CompletionChunk.from_wire(answered, tools_by_name)
|
|
71
|
+
yield chunk
|
|
72
|
+
|
|
73
|
+
async def last_hidden_states(
|
|
74
|
+
self, parts: list[str], continuations_of_each_part: list[list[str]]
|
|
75
|
+
) -> AsyncIterator[list[list[float]]]:
|
|
76
|
+
body = {"parts": parts, "continuations_of_each_part": continuations_of_each_part}
|
|
77
|
+
last_hidden_states_url = f"{self.url}/last_hidden_states"
|
|
78
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
79
|
+
async with client.stream("POST", last_hidden_states_url, json=body) as response:
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
async for line in response.aiter_lines():
|
|
82
|
+
answered = json.loads(line)
|
|
83
|
+
yield answered["last_hidden_states"]
|
|
@@ -15,7 +15,6 @@ task whoever made it.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import base64
|
|
18
|
-
from dataclasses import dataclass
|
|
19
18
|
from typing import Any
|
|
20
19
|
|
|
21
20
|
import httpx
|
|
@@ -23,17 +22,9 @@ import httpx
|
|
|
23
22
|
from cortexgrid_infer.core import GeneratedImage, GeneratingModel
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
@dataclass
|
|
27
25
|
class ServedGeneratingModel(GeneratingModel):
|
|
28
26
|
"""The client of any text-to-image serve app deployed at `url`."""
|
|
29
27
|
|
|
30
|
-
url: str
|
|
31
|
-
model_id: str
|
|
32
|
-
|
|
33
|
-
@property
|
|
34
|
-
def name(self) -> str:
|
|
35
|
-
return self.model_id
|
|
36
|
-
|
|
37
28
|
async def generate(
|
|
38
29
|
self,
|
|
39
30
|
prompt: str,
|
|
@@ -69,6 +60,5 @@ class ServedGeneratingModel(GeneratingModel):
|
|
|
69
60
|
"guidance": data.get("guidance", guidance),
|
|
70
61
|
"seed": data.get("seed", seed),
|
|
71
62
|
"duration_s": data.get("duration_s"),
|
|
72
|
-
"model_id": self.model_id,
|
|
73
63
|
},
|
|
74
64
|
)
|
|
@@ -15,7 +15,6 @@ Every such app speaks it through
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import base64
|
|
18
|
-
from dataclasses import dataclass
|
|
19
18
|
from typing import Any
|
|
20
19
|
|
|
21
20
|
import httpx
|
|
@@ -41,17 +40,9 @@ def decode(data: str, kind: tuple[str, int]) -> np.ndarray:
|
|
|
41
40
|
return np.frombuffer(base64.b64decode(data), dtype=dtype).reshape(-1, columns)
|
|
42
41
|
|
|
43
42
|
|
|
44
|
-
@dataclass
|
|
45
43
|
class ServedMeshingModel(MeshingModel):
|
|
46
44
|
"""The client of any image-to-mesh serve app deployed at `url`."""
|
|
47
45
|
|
|
48
|
-
url: str
|
|
49
|
-
model_id: str
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def name(self) -> str:
|
|
53
|
-
return self.model_id
|
|
54
|
-
|
|
55
46
|
async def mesh(self, image: bytes, **options: Any) -> GeneratedMesh:
|
|
56
47
|
body: dict[str, Any] = {"image": base64.b64encode(image).decode("ascii"), **options}
|
|
57
48
|
# Meshing takes seconds; no client-side timeout.
|
|
@@ -67,6 +58,5 @@ class ServedMeshingModel(MeshingModel):
|
|
|
67
58
|
params={
|
|
68
59
|
**data.get("params", {}),
|
|
69
60
|
"duration_s": data.get("duration_s"),
|
|
70
|
-
"model_id": self.model_id,
|
|
71
61
|
},
|
|
72
62
|
)
|
|
@@ -15,7 +15,6 @@ every model of the task whoever made it.
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
-
from dataclasses import dataclass
|
|
19
18
|
from typing import Any
|
|
20
19
|
|
|
21
20
|
import httpx
|
|
@@ -23,17 +22,9 @@ import httpx
|
|
|
23
22
|
from cortexgrid_infer.core import RewritingModel
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
@dataclass
|
|
27
25
|
class ServedRewritingModel(RewritingModel):
|
|
28
26
|
"""The client of any rewriting serve app deployed at `url`."""
|
|
29
27
|
|
|
30
|
-
url: str
|
|
31
|
-
model_id: str
|
|
32
|
-
|
|
33
|
-
@property
|
|
34
|
-
def name(self) -> str:
|
|
35
|
-
return self.model_id
|
|
36
|
-
|
|
37
28
|
async def rewrite(
|
|
38
29
|
self, text: str, max_new_tokens: int = 512, **generation_options: Any
|
|
39
30
|
) -> str:
|
|
@@ -5,9 +5,9 @@ cortexgrid stores whatever weights it is handed under whatever key it is given,
|
|
|
5
5
|
and serves them with whatever class it was told to bundle. It deliberately knows
|
|
6
6
|
nothing about where a model came from or what shape it is. A `ModelEntry`
|
|
7
7
|
supplies exactly that missing knowledge - the registry identity, the serve app,
|
|
8
|
-
the hardware it needs, its config
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
the hardware it needs, and its config - and drives no lifecycle of its own:
|
|
9
|
+
importing, deploying and deleting stay the caller's calls against the cortexgrid
|
|
10
|
+
SDK, and a deployment's client is the deployment's `client()`.
|
|
11
11
|
|
|
12
12
|
A model with weights is an importer's (`cortexgrid_infer.importers`) and goes
|
|
13
13
|
through `import_model`. A model hosted elsewhere is a `Hosted` entry and goes
|
|
@@ -27,7 +27,6 @@ import abc
|
|
|
27
27
|
|
|
28
28
|
import cortexgrid
|
|
29
29
|
|
|
30
|
-
from cortexgrid_infer.core import DeployedModel
|
|
31
30
|
from cortexgrid_infer.serve_apps.base import HostedModel
|
|
32
31
|
|
|
33
32
|
|
|
@@ -49,8 +48,8 @@ def split_model_id(model_id: str) -> tuple[str, str]:
|
|
|
49
48
|
class ModelEntry(abc.ABC):
|
|
50
49
|
"""One model as the registry files it and cortexgrid serves it."""
|
|
51
50
|
|
|
52
|
-
# The id the model is known by where it came from; the registry key
|
|
53
|
-
#
|
|
51
|
+
# The id the model is known by where it came from; the registry key derives
|
|
52
|
+
# from it.
|
|
54
53
|
model_id: str
|
|
55
54
|
|
|
56
55
|
# The class cortexgrid bundles and instantiates on the cluster.
|
|
@@ -72,10 +71,6 @@ class ModelEntry(abc.ABC):
|
|
|
72
71
|
bundled code already knows everything it needs."""
|
|
73
72
|
return {}
|
|
74
73
|
|
|
75
|
-
@abc.abstractmethod
|
|
76
|
-
def client(self, url: str) -> DeployedModel:
|
|
77
|
-
"""Return a client for this model's serve app, deployed at `url`."""
|
|
78
|
-
|
|
79
74
|
|
|
80
75
|
class Hosted(ModelEntry):
|
|
81
76
|
"""A model hosted elsewhere, served by a `HostedModel` app that forwards to it.
|
|
@@ -97,6 +92,3 @@ class Hosted(ModelEntry):
|
|
|
97
92
|
|
|
98
93
|
def config(self) -> dict[str, str]:
|
|
99
94
|
return dict(self._config)
|
|
100
|
-
|
|
101
|
-
def client(self, url: str) -> DeployedModel:
|
|
102
|
-
return self.serve_app.client(url, self.model_id)
|
|
@@ -19,12 +19,13 @@ from typing import Any
|
|
|
19
19
|
import cortexgrid
|
|
20
20
|
from anthropic import AsyncAnthropic
|
|
21
21
|
from cortexgrid import serve
|
|
22
|
-
from fastapi import FastAPI
|
|
22
|
+
from fastapi import FastAPI, HTTPException, status
|
|
23
23
|
from fastapi.responses import StreamingResponse
|
|
24
24
|
|
|
25
25
|
from cortexgrid_infer.core import Message, ToolSpec
|
|
26
|
-
from cortexgrid_infer.protocols.completion import ServedCompletingModel
|
|
26
|
+
from cortexgrid_infer.protocols.completion import ServedCompletingModel
|
|
27
27
|
from cortexgrid_infer.serve_apps.base import HostedModel
|
|
28
|
+
from cortexgrid_infer.serve_apps.streamed_completion import StreamedCompletionWriter
|
|
28
29
|
|
|
29
30
|
_app = FastAPI()
|
|
30
31
|
|
|
@@ -128,8 +129,8 @@ class AnthropicText2Text(HostedModel):
|
|
|
128
129
|
return {MODEL_PARAM: model_id, API_KEY_SECRET_PARAM: api_key_secret}
|
|
129
130
|
|
|
130
131
|
@classmethod
|
|
131
|
-
def client(cls,
|
|
132
|
-
return ServedCompletingModel(
|
|
132
|
+
def client(cls, deployment: cortexgrid.Deployment[ServedCompletingModel]) -> ServedCompletingModel:
|
|
133
|
+
return ServedCompletingModel(key=deployment.key, url=deployment.url)
|
|
133
134
|
|
|
134
135
|
def __init__(self, deployment: cortexgrid.DeploymentKey) -> None:
|
|
135
136
|
config = cortexgrid.model_config(deployment)
|
|
@@ -162,19 +163,30 @@ class AnthropicText2Text(HostedModel):
|
|
|
162
163
|
create_kwargs["tools"] = tools
|
|
163
164
|
|
|
164
165
|
async def stream() -> AsyncIterator[bytes]:
|
|
166
|
+
writer = StreamedCompletionWriter()
|
|
165
167
|
async with self._client.messages.stream(**create_kwargs) as events:
|
|
166
168
|
async for event in events:
|
|
167
169
|
if (
|
|
168
170
|
event.type == "content_block_delta"
|
|
169
171
|
and event.delta.type == "text_delta"
|
|
170
172
|
):
|
|
171
|
-
|
|
173
|
+
line = writer.read_content(event.delta.text)
|
|
174
|
+
yield line
|
|
172
175
|
|
|
173
176
|
elif event.type == "content_block_stop":
|
|
174
177
|
block = events.current_message_snapshot.content[event.index]
|
|
175
178
|
if block.type == "tool_use":
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
arguments = dict(block.input)
|
|
180
|
+
line = writer.read_tool_call(block.name, arguments, block.id)
|
|
181
|
+
yield line
|
|
182
|
+
for line in writer.finish():
|
|
183
|
+
yield line
|
|
184
|
+
|
|
185
|
+
return StreamingResponse(stream(), media_type="application/x-ndjson")
|
|
186
|
+
|
|
187
|
+
@_app.post("/last_hidden_states")
|
|
188
|
+
async def last_hidden_states(self, body: dict[str, Any]) -> None:
|
|
189
|
+
raise HTTPException(
|
|
190
|
+
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
191
|
+
detail="Anthropic does not expose its models' hidden states",
|
|
192
|
+
)
|