cortexgrid-infer 0.2.4__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/PKG-INFO +23 -12
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/README.md +20 -10
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/__init__.py +12 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/core.py +27 -0
- cortexgrid_infer-0.2.7/cortexgrid_infer/meshing.py +72 -0
- cortexgrid_infer-0.2.7/cortexgrid_infer/meshing_serve.py +72 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/huggingface_complete_serve.py +38 -15
- cortexgrid_infer-0.2.7/cortexgrid_infer/providers/huggingface_mesh.py +37 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/pyproject.toml +4 -2
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/.gitignore +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/LICENSE +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/compiling.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/completion.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/device.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/importing.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/__init__.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/anthropic.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/anthropic_serve.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/huggingface_complete.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/huggingface_image.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/huggingface_image_serve.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/py.typed +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/requirements.py +0 -0
- {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/utils.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cortexgrid-infer
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: Deploy and interact with models across providers (Anthropic, HuggingFace LLM + diffusion)
|
|
3
|
+
Version: 0.2.7
|
|
4
|
+
Summary: Deploy and interact with models across providers (Anthropic, HuggingFace LLM + diffusion + image-to-mesh)
|
|
5
5
|
Project-URL: Homepage, https://github.com/robodatalab/cortexgrid-infer
|
|
6
6
|
Project-URL: Repository, https://github.com/robodatalab/cortexgrid-infer
|
|
7
7
|
License-Expression: Apache-2.0
|
|
@@ -14,6 +14,7 @@ Requires-Dist: diffusers>=0.38
|
|
|
14
14
|
Requires-Dist: fastapi>=0.133
|
|
15
15
|
Requires-Dist: httpx>=0.27.0
|
|
16
16
|
Requires-Dist: huggingface-hub>=0.20.0
|
|
17
|
+
Requires-Dist: numpy>=1.24
|
|
17
18
|
Requires-Dist: pillow>=10.0
|
|
18
19
|
Requires-Dist: protobuf>=4.0
|
|
19
20
|
Requires-Dist: python-dotenv>=1.0.0
|
|
@@ -60,6 +61,7 @@ An **importer** is the whole of this library's cluster-side surface. It gives yo
|
|
|
60
61
|
| `HuggingFaceCompletingImport(hf_id, token=None)` | HuggingFace causal LM, via `AutoModelForCausalLM` | `ServedCompletingModel` (`complete`) |
|
|
61
62
|
| `HuggingFaceImageImport(hf_id, token=None, ignore_patterns=None)` | HuggingFace diffusers pipeline | `HuggingFaceImageModel` (`generate`) |
|
|
62
63
|
| `AnthropicImport(model_id, api_key_secret=...)` | the Anthropic API, forwarded from the cluster | `ServedCompletingModel` (`complete`) |
|
|
64
|
+
| `HuggingFaceMeshImport(hf_id, token=None, ignore_patterns=None)` — a base: subclass it, naming a `MeshingDeployment` subclass as `serve_app` | an image-to-mesh model, run by the serve app the subclass supplies (`load` and `make_mesh`) behind the family's `POST /mesh` | `ServedMeshingModel` (`mesh`: one picture in, a `GeneratedMesh` out) |
|
|
63
65
|
|
|
64
66
|
Not every model has weights, so `.source` belongs to the HuggingFace importers
|
|
65
67
|
rather than to all of them: one that has weights goes through
|
|
@@ -184,16 +186,25 @@ Both serve apps are tuned rather than defaulted:
|
|
|
184
186
|
itself, capturing prefill and decode separately, which it is better placed to
|
|
185
187
|
do; compiling the module here as well would compile the same forward twice.
|
|
186
188
|
|
|
187
|
-
The fixed length is the point, and
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
189
|
+
The fixed length is the point, and how long it is decides what decode costs:
|
|
190
|
+
every token's attention reads the whole cache, filled or not. That is why it is
|
|
191
|
+
*not* pinned to the model's context — 32k slots of a Qwen2.5-3B KV cache is
|
|
192
|
+
1.2 GB of keys and values re-read per generated token, several times the
|
|
193
|
+
traffic of the model's own weights, to make room for a prompt nobody sent. It
|
|
194
|
+
is pinned instead to the prompt and reply a caller plausibly sends: 4096 tokens,
|
|
195
|
+
or `max_total_tokens` on the model card, never past the model's own
|
|
196
|
+
`max_position_embeddings`. A reply takes at most half of it, so an
|
|
197
|
+
over-optimistic `max_new_tokens` cannot squeeze the prompt to nothing, and a
|
|
198
|
+
prompt that will not fit in the rest is truncated — keeping the end, since a
|
|
199
|
+
chat template puts the turn to answer last — with a warning naming both
|
|
200
|
+
lengths. Serving it whole would resize the cache and recompile the decode loop,
|
|
201
|
+
which costs more than the generation itself.
|
|
202
|
+
|
|
203
|
+
The image app asks to be configured for nothing, for the same reason neither asks
|
|
204
|
+
which dtype to load in; the completion app takes `max_total_tokens` alone, because
|
|
205
|
+
how much cache a caller needs is the one thing the model cannot tell it. Both
|
|
206
|
+
happen on CUDA and nowhere else — inductor is weakest off it, and a host
|
|
207
|
+
outrunning its accelerator is a GPU problem to begin with.
|
|
197
208
|
|
|
198
209
|
Two things worth knowing:
|
|
199
210
|
|
|
@@ -34,6 +34,7 @@ An **importer** is the whole of this library's cluster-side surface. It gives yo
|
|
|
34
34
|
| `HuggingFaceCompletingImport(hf_id, token=None)` | HuggingFace causal LM, via `AutoModelForCausalLM` | `ServedCompletingModel` (`complete`) |
|
|
35
35
|
| `HuggingFaceImageImport(hf_id, token=None, ignore_patterns=None)` | HuggingFace diffusers pipeline | `HuggingFaceImageModel` (`generate`) |
|
|
36
36
|
| `AnthropicImport(model_id, api_key_secret=...)` | the Anthropic API, forwarded from the cluster | `ServedCompletingModel` (`complete`) |
|
|
37
|
+
| `HuggingFaceMeshImport(hf_id, token=None, ignore_patterns=None)` — a base: subclass it, naming a `MeshingDeployment` subclass as `serve_app` | an image-to-mesh model, run by the serve app the subclass supplies (`load` and `make_mesh`) behind the family's `POST /mesh` | `ServedMeshingModel` (`mesh`: one picture in, a `GeneratedMesh` out) |
|
|
37
38
|
|
|
38
39
|
Not every model has weights, so `.source` belongs to the HuggingFace importers
|
|
39
40
|
rather than to all of them: one that has weights goes through
|
|
@@ -158,16 +159,25 @@ Both serve apps are tuned rather than defaulted:
|
|
|
158
159
|
itself, capturing prefill and decode separately, which it is better placed to
|
|
159
160
|
do; compiling the module here as well would compile the same forward twice.
|
|
160
161
|
|
|
161
|
-
The fixed length is the point, and
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
162
|
+
The fixed length is the point, and how long it is decides what decode costs:
|
|
163
|
+
every token's attention reads the whole cache, filled or not. That is why it is
|
|
164
|
+
*not* pinned to the model's context — 32k slots of a Qwen2.5-3B KV cache is
|
|
165
|
+
1.2 GB of keys and values re-read per generated token, several times the
|
|
166
|
+
traffic of the model's own weights, to make room for a prompt nobody sent. It
|
|
167
|
+
is pinned instead to the prompt and reply a caller plausibly sends: 4096 tokens,
|
|
168
|
+
or `max_total_tokens` on the model card, never past the model's own
|
|
169
|
+
`max_position_embeddings`. A reply takes at most half of it, so an
|
|
170
|
+
over-optimistic `max_new_tokens` cannot squeeze the prompt to nothing, and a
|
|
171
|
+
prompt that will not fit in the rest is truncated — keeping the end, since a
|
|
172
|
+
chat template puts the turn to answer last — with a warning naming both
|
|
173
|
+
lengths. Serving it whole would resize the cache and recompile the decode loop,
|
|
174
|
+
which costs more than the generation itself.
|
|
175
|
+
|
|
176
|
+
The image app asks to be configured for nothing, for the same reason neither asks
|
|
177
|
+
which dtype to load in; the completion app takes `max_total_tokens` alone, because
|
|
178
|
+
how much cache a caller needs is the one thing the model cannot tell it. Both
|
|
179
|
+
happen on CUDA and nowhere else — inductor is weakest off it, and a host
|
|
180
|
+
outrunning its accelerator is a GPU problem to begin with.
|
|
171
181
|
|
|
172
182
|
Two things worth knowing:
|
|
173
183
|
|
|
@@ -26,13 +26,16 @@ from cortexgrid_infer.core import (
|
|
|
26
26
|
DeployedModel,
|
|
27
27
|
CompletingModel,
|
|
28
28
|
GeneratedImage,
|
|
29
|
+
GeneratedMesh,
|
|
29
30
|
GeneratingModel,
|
|
31
|
+
MeshingModel,
|
|
30
32
|
Message,
|
|
31
33
|
Tool,
|
|
32
34
|
ToolCall,
|
|
33
35
|
ToolSpec,
|
|
34
36
|
complete,
|
|
35
37
|
generate,
|
|
38
|
+
mesh,
|
|
36
39
|
)
|
|
37
40
|
from cortexgrid_infer.completion import ServedCompletingModel
|
|
38
41
|
from cortexgrid_infer.device import detect_device
|
|
@@ -43,19 +46,25 @@ from cortexgrid_infer.providers.huggingface_image import (
|
|
|
43
46
|
HuggingFaceImageImport,
|
|
44
47
|
HuggingFaceImageModel,
|
|
45
48
|
)
|
|
49
|
+
from cortexgrid_infer.meshing import ServedMeshingModel
|
|
50
|
+
from cortexgrid_infer.meshing_serve import MeshingDeployment
|
|
51
|
+
from cortexgrid_infer.providers.huggingface_mesh import HuggingFaceMeshImport
|
|
46
52
|
|
|
47
53
|
__all__ = [
|
|
48
54
|
"CompletionChunk",
|
|
49
55
|
"DeployedModel",
|
|
50
56
|
"CompletingModel",
|
|
51
57
|
"GeneratedImage",
|
|
58
|
+
"GeneratedMesh",
|
|
52
59
|
"GeneratingModel",
|
|
60
|
+
"MeshingModel",
|
|
53
61
|
"Message",
|
|
54
62
|
"Tool",
|
|
55
63
|
"ToolCall",
|
|
56
64
|
"ToolSpec",
|
|
57
65
|
"complete",
|
|
58
66
|
"generate",
|
|
67
|
+
"mesh",
|
|
59
68
|
"ModelDeployFailed",
|
|
60
69
|
"detect_device",
|
|
61
70
|
"split_model_id",
|
|
@@ -63,7 +72,10 @@ __all__ = [
|
|
|
63
72
|
"HuggingFaceImport",
|
|
64
73
|
"HuggingFaceCompletingImport",
|
|
65
74
|
"HuggingFaceImageImport",
|
|
75
|
+
"HuggingFaceMeshImport",
|
|
66
76
|
"AnthropicImport",
|
|
67
77
|
"ServedCompletingModel",
|
|
68
78
|
"HuggingFaceImageModel",
|
|
79
|
+
"ServedMeshingModel",
|
|
80
|
+
"MeshingDeployment",
|
|
69
81
|
]
|
|
@@ -8,6 +8,8 @@ from functools import partial
|
|
|
8
8
|
from collections.abc import AsyncIterator
|
|
9
9
|
from typing import Any, Callable, Sequence
|
|
10
10
|
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
@dataclass
|
|
13
15
|
class ToolCall:
|
|
@@ -87,6 +89,26 @@ class GeneratingModel(DeployedModel):
|
|
|
87
89
|
) -> GeneratedImage: ...
|
|
88
90
|
|
|
89
91
|
|
|
92
|
+
@dataclass
|
|
93
|
+
class GeneratedMesh:
|
|
94
|
+
"""One mesh: `vertices` (N, 3) float32, x to the right of the picture it was
|
|
95
|
+
made from, y up, z toward whoever looked at it; `faces` (M, 3) int32 indices
|
|
96
|
+
into them, counter-clockwise seen from outside; `colours` (N, 3) uint8, each
|
|
97
|
+
vertex's colour. Plus the resolved parameters the deployment used."""
|
|
98
|
+
|
|
99
|
+
vertices: np.ndarray
|
|
100
|
+
faces: np.ndarray
|
|
101
|
+
colours: np.ndarray
|
|
102
|
+
params: dict[str, Any] = field(default_factory=dict)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class MeshingModel(DeployedModel):
|
|
106
|
+
"""A deployed model that makes a mesh of the object in one picture."""
|
|
107
|
+
|
|
108
|
+
@abc.abstractmethod
|
|
109
|
+
async def mesh(self, image: bytes, **kwargs: Any) -> GeneratedMesh: ...
|
|
110
|
+
|
|
111
|
+
|
|
90
112
|
async def complete(
|
|
91
113
|
deployed_model: CompletingModel,
|
|
92
114
|
messages: list[Message],
|
|
@@ -115,3 +137,8 @@ async def generate(
|
|
|
115
137
|
) -> GeneratedImage:
|
|
116
138
|
"""Single image-generation request to the deployed model."""
|
|
117
139
|
return await deployed_model.generate(prompt, image=image, **kwargs)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def mesh(deployed_model: MeshingModel, image: bytes, **kwargs: Any) -> GeneratedMesh:
|
|
143
|
+
"""Single image-to-mesh request to the deployed model."""
|
|
144
|
+
return await deployed_model.mesh(image, **kwargs)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""The wire protocol between a cortexgrid-served image-to-mesh app and its client.
|
|
2
|
+
|
|
3
|
+
An image-to-mesh serve app answers `POST /mesh`: the request carries a picture
|
|
4
|
+
of an object as a base64 PNG under `image`, plus whatever options the model
|
|
5
|
+
takes (a marching-cubes resolution, say) as further fields. The reply carries
|
|
6
|
+
the mesh as base64 little-endian arrays - `vertices` float32 x, y, z, `faces`
|
|
7
|
+
int32 indices, `colours` uint8 r, g, b per vertex - in the frame
|
|
8
|
+
`GeneratedMesh` documents, and `params`, what the model resolved.
|
|
9
|
+
|
|
10
|
+
Every such app speaks it through `cortexgrid_infer.meshing_serve.MeshingDeployment`,
|
|
11
|
+
so one client, `ServedMeshingModel`, serves every model of the family whoever
|
|
12
|
+
made it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from cortexgrid_infer.core import GeneratedMesh, MeshingModel
|
|
25
|
+
|
|
26
|
+
# How each array travels: its type, and how many values make one row.
|
|
27
|
+
VERTICES = ("<f4", 3)
|
|
28
|
+
FACES = ("<i4", 3)
|
|
29
|
+
COLOURS = ("u1", 3)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def encode(array: np.ndarray, kind: tuple[str, int]) -> str:
|
|
33
|
+
"""`array` as the protocol sends it."""
|
|
34
|
+
dtype, _ = kind
|
|
35
|
+
return base64.b64encode(np.ascontiguousarray(array, dtype=dtype).tobytes()).decode("ascii")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def decode(data: str, kind: tuple[str, int]) -> np.ndarray:
|
|
39
|
+
"""An array the protocol sent, one row per vertex or triangle."""
|
|
40
|
+
dtype, columns = kind
|
|
41
|
+
return np.frombuffer(base64.b64decode(data), dtype=dtype).reshape(-1, columns)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ServedMeshingModel(MeshingModel):
|
|
46
|
+
"""The client of any image-to-mesh serve app deployed at `url`."""
|
|
47
|
+
|
|
48
|
+
url: str
|
|
49
|
+
model_id: str
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def name(self) -> str:
|
|
53
|
+
return self.model_id
|
|
54
|
+
|
|
55
|
+
async def mesh(self, image: bytes, **options: Any) -> GeneratedMesh:
|
|
56
|
+
body: dict[str, Any] = {"image": base64.b64encode(image).decode("ascii"), **options}
|
|
57
|
+
# Meshing takes seconds; no client-side timeout.
|
|
58
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
59
|
+
response = await client.post(f"{self.url}/mesh", json=body)
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
data = response.json()
|
|
62
|
+
|
|
63
|
+
return GeneratedMesh(
|
|
64
|
+
vertices=decode(data["vertices"], VERTICES),
|
|
65
|
+
faces=decode(data["faces"], FACES),
|
|
66
|
+
colours=decode(data["colours"], COLOURS),
|
|
67
|
+
params={
|
|
68
|
+
**data.get("params", {}),
|
|
69
|
+
"duration_s": data.get("duration_s"),
|
|
70
|
+
"model_id": self.model_id,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""The base of every image-to-mesh serve app: the `POST /mesh` route and loading the
|
|
2
|
+
model's files from the cortexgrid registry, around the two steps that differ per
|
|
3
|
+
model.
|
|
4
|
+
|
|
5
|
+
There is no one loader for image-to-mesh models the way `AutoModelForCausalLM`
|
|
6
|
+
or a diffusers `model_index.json` serves a whole family of repos: each model
|
|
7
|
+
ships its own code. So a model's serve app subclasses `MeshingDeployment` and
|
|
8
|
+
supplies `load` and `make_mesh`; the subclass inherits the route, whose protocol
|
|
9
|
+
is `cortexgrid_infer.meshing`'s. cortexgrid bundles the subclass's own file, so
|
|
10
|
+
the model's code travels with it.
|
|
11
|
+
|
|
12
|
+
class MyMeshDeployment(MeshingDeployment):
|
|
13
|
+
def load(self, path, device): ...
|
|
14
|
+
def make_mesh(self, image, **options) -> GeneratedMesh: ...
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import base64
|
|
20
|
+
import io
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import cortexgrid
|
|
26
|
+
from cortexgrid import serve
|
|
27
|
+
from fastapi import FastAPI
|
|
28
|
+
from PIL import Image, ImageOps
|
|
29
|
+
import torch
|
|
30
|
+
|
|
31
|
+
from cortexgrid_infer import meshing
|
|
32
|
+
from cortexgrid_infer.core import GeneratedMesh
|
|
33
|
+
from cortexgrid_infer.device import detect_device
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_app = FastAPI()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@serve.ingress(_app)
|
|
40
|
+
class MeshingDeployment:
|
|
41
|
+
"""One replica of an image-to-mesh model."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, family: str, suffix: str, run_name: str) -> None:
|
|
44
|
+
self.device = detect_device()
|
|
45
|
+
self.load(Path(cortexgrid.load_model(family, suffix, run_name)), self.device)
|
|
46
|
+
|
|
47
|
+
def load(self, path: Path, device: torch.device) -> None:
|
|
48
|
+
"""Build the model from its files at `path`, on `device`."""
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
|
|
51
|
+
def make_mesh(self, image: Image.Image, **options: Any) -> GeneratedMesh:
|
|
52
|
+
"""The mesh of the object in `image`, in the frame `GeneratedMesh`
|
|
53
|
+
documents, with the options the request carried; `params` says what the
|
|
54
|
+
model resolved them to."""
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
@_app.post("/mesh")
|
|
58
|
+
async def mesh(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
59
|
+
image = ImageOps.exif_transpose(Image.open(io.BytesIO(base64.b64decode(body["image"]))))
|
|
60
|
+
options = {key: value for key, value in body.items() if key != "image" and value is not None}
|
|
61
|
+
|
|
62
|
+
t0 = time.time()
|
|
63
|
+
made = self.make_mesh(image, **options)
|
|
64
|
+
duration = time.time() - t0
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"vertices": meshing.encode(made.vertices, meshing.VERTICES),
|
|
68
|
+
"faces": meshing.encode(made.faces, meshing.FACES),
|
|
69
|
+
"colours": meshing.encode(made.colours, meshing.COLOURS),
|
|
70
|
+
"params": made.params,
|
|
71
|
+
"duration_s": round(duration, 2),
|
|
72
|
+
}
|
|
@@ -29,8 +29,14 @@ log = logging.getLogger(__name__)
|
|
|
29
29
|
_app = FastAPI()
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
|
|
32
|
+
# Prompt + reply a replica is sized for. The static cache is allocated at this
|
|
33
|
+
# many slots and every decoded token reads all of them, filled or not: at
|
|
34
|
+
# Qwen2.5-3B's 32k context that is 1.2 GB of keys and values re-read per token,
|
|
35
|
+
# which costs far more than the prompt it makes room for. So the default is what
|
|
36
|
+
# a caller plausibly sends rather than what the model could hold; a model that
|
|
37
|
+
# needs more takes `max_total_tokens` on its model card, up to its own context.
|
|
38
|
+
MAX_TOTAL_TOKENS_PARAM = "max_total_tokens"
|
|
39
|
+
DEFAULT_MAX_TOTAL_TOKENS = 4096
|
|
34
40
|
|
|
35
41
|
_RESERVED_BODY_KEYS = {
|
|
36
42
|
"messages",
|
|
@@ -55,8 +61,12 @@ class HuggingFaceCompletingDeployment:
|
|
|
55
61
|
str(path), torch_dtype=torch.float16
|
|
56
62
|
)
|
|
57
63
|
self._model.to(self._device) # type: ignore
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
settings = cortexgrid.model_config(family, suffix, run_name)
|
|
65
|
+
context = getattr(
|
|
66
|
+
self._model.config, "max_position_embeddings", DEFAULT_MAX_TOTAL_TOKENS
|
|
67
|
+
)
|
|
68
|
+
self._max_total_tokens = min(
|
|
69
|
+
int(settings.get(MAX_TOTAL_TOKENS_PARAM, DEFAULT_MAX_TOTAL_TOKENS)), context
|
|
60
70
|
)
|
|
61
71
|
# A static cache of fixed length is what makes decode compilable: it
|
|
62
72
|
# pins the shape every forward sees, so the loop is captured once
|
|
@@ -66,27 +76,39 @@ class HuggingFaceCompletingDeployment:
|
|
|
66
76
|
if self._compiled:
|
|
67
77
|
config = self._model.generation_config
|
|
68
78
|
config.cache_implementation = "static"
|
|
69
|
-
config.max_cache_len = self.
|
|
79
|
+
config.max_cache_len = self._max_total_tokens
|
|
70
80
|
config.compile_config = CompileConfig(mode=compiling.Mode.GRAPHED)
|
|
71
81
|
|
|
72
|
-
def
|
|
73
|
-
"""
|
|
82
|
+
def _room_for_the_reply(self, requested: int) -> int:
|
|
83
|
+
"""How many tokens a reply may take, leaving the prompt the rest.
|
|
84
|
+
|
|
85
|
+
Half the budget at most, so a caller asking for more output than the
|
|
86
|
+
replica is sized for cannot squeeze the prompt down to nothing."""
|
|
87
|
+
return min(requested, self._max_total_tokens // 2)
|
|
88
|
+
|
|
89
|
+
def _truncate(self, inputs: Any, limit: int) -> Any:
|
|
90
|
+
"""Cut an over-long prompt to ``limit`` tokens, keeping its end.
|
|
91
|
+
|
|
92
|
+
A prompt and reply that together outgrew the cache would reallocate it
|
|
93
|
+
and recompile the decode loop, which costs more than the generation."""
|
|
74
94
|
length = inputs["input_ids"].shape[-1]
|
|
75
|
-
if length <=
|
|
95
|
+
if length <= limit:
|
|
76
96
|
return inputs
|
|
77
97
|
log.warning(
|
|
78
|
-
"truncating prompt from %d to %d tokens, the
|
|
79
|
-
length,
|
|
98
|
+
"truncating prompt from %d to %d tokens, the room this replica has",
|
|
99
|
+
length, limit,
|
|
80
100
|
)
|
|
81
101
|
for key, value in inputs.items():
|
|
82
|
-
inputs[key] = value[..., -
|
|
102
|
+
inputs[key] = value[..., -limit:]
|
|
83
103
|
return inputs
|
|
84
104
|
|
|
85
105
|
@_app.post("/complete")
|
|
86
106
|
async def complete(self, body: dict[str, Any]) -> StreamingResponse:
|
|
87
107
|
messages = body["messages"]
|
|
88
108
|
tools = body.get("tools")
|
|
89
|
-
max_new_tokens =
|
|
109
|
+
max_new_tokens = self._room_for_the_reply(
|
|
110
|
+
body.get("max_new_tokens", self._max_total_tokens)
|
|
111
|
+
)
|
|
90
112
|
temperature = body.get("temperature", 0.7)
|
|
91
113
|
top_p = body.get("top_p", 0.9)
|
|
92
114
|
top_k = body.get("top_k", 50)
|
|
@@ -99,9 +121,10 @@ class HuggingFaceCompletingDeployment:
|
|
|
99
121
|
tokenize=False,
|
|
100
122
|
add_generation_prompt=True,
|
|
101
123
|
)
|
|
102
|
-
inputs = self._truncate(
|
|
103
|
-
self.
|
|
104
|
-
|
|
124
|
+
inputs = self._truncate(
|
|
125
|
+
self._tokenizer(prompt, return_tensors="pt"),
|
|
126
|
+
self._max_total_tokens - max_new_tokens,
|
|
127
|
+
).to(self._model.device)
|
|
105
128
|
|
|
106
129
|
queue: asyncio.Queue[str | None] = asyncio.Queue()
|
|
107
130
|
error: list[BaseException] = []
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""HuggingFace image-to-mesh provider: the importer an image-to-mesh model on the
|
|
2
|
+
Hub is taken into the cortexgrid registry with, and the client of what it serves.
|
|
3
|
+
|
|
4
|
+
Each such model brings its own code (see `cortexgrid_infer.meshing_serve`), so
|
|
5
|
+
this is a base: a model's own importer subclasses it and names its serve app.
|
|
6
|
+
|
|
7
|
+
class MyMeshImport(HuggingFaceMeshImport):
|
|
8
|
+
serve_app = MyMeshDeployment # a MeshingDeployment subclass
|
|
9
|
+
vram_gb = 6.0
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import dataclasses
|
|
15
|
+
from typing import ClassVar
|
|
16
|
+
|
|
17
|
+
import cortexgrid
|
|
18
|
+
|
|
19
|
+
from cortexgrid_infer.importing import HuggingFaceImport
|
|
20
|
+
from cortexgrid_infer.meshing import ServedMeshingModel
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HuggingFaceMeshImport(HuggingFaceImport):
|
|
24
|
+
"""What `cortexgrid.import_model` needs to take an image-to-mesh model from
|
|
25
|
+
the Hub, less the serve app, which the subclass names."""
|
|
26
|
+
|
|
27
|
+
# What one replica needs at least, whatever the weights come to: meshing
|
|
28
|
+
# queries the model over a whole 3D grid, which takes memory the weights do
|
|
29
|
+
# not show.
|
|
30
|
+
vram_gb: ClassVar[float] = 0.0
|
|
31
|
+
|
|
32
|
+
def requirements(self) -> cortexgrid.ModelRequirements:
|
|
33
|
+
estimate = super().requirements()
|
|
34
|
+
return dataclasses.replace(estimate, vram_gb=max(estimate.vram_gb, self.vram_gb))
|
|
35
|
+
|
|
36
|
+
def client(self, url: str) -> ServedMeshingModel:
|
|
37
|
+
return ServedMeshingModel(url=url, model_id=self.hf_id)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cortexgrid-infer"
|
|
3
|
-
version = "0.2.
|
|
4
|
-
description = "Deploy and interact with models across providers (Anthropic, HuggingFace LLM + diffusion)"
|
|
3
|
+
version = "0.2.7"
|
|
4
|
+
description = "Deploy and interact with models across providers (Anthropic, HuggingFace LLM + diffusion + image-to-mesh)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
@@ -34,6 +34,8 @@ dependencies = [
|
|
|
34
34
|
"accelerate>=0.33",
|
|
35
35
|
"sentencepiece>=0.2",
|
|
36
36
|
"protobuf>=4.0",
|
|
37
|
+
# Image-to-mesh: meshes travel as numpy arrays (core.GeneratedMesh).
|
|
38
|
+
"numpy>=1.24",
|
|
37
39
|
]
|
|
38
40
|
|
|
39
41
|
[project.optional-dependencies]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/anthropic_serve.py
RENAMED
|
File without changes
|
{cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/huggingface_complete.py
RENAMED
|
File without changes
|
{cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/huggingface_image.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|