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.
Files changed (24) hide show
  1. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/PKG-INFO +23 -12
  2. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/README.md +20 -10
  3. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/__init__.py +12 -0
  4. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/core.py +27 -0
  5. cortexgrid_infer-0.2.7/cortexgrid_infer/meshing.py +72 -0
  6. cortexgrid_infer-0.2.7/cortexgrid_infer/meshing_serve.py +72 -0
  7. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/huggingface_complete_serve.py +38 -15
  8. cortexgrid_infer-0.2.7/cortexgrid_infer/providers/huggingface_mesh.py +37 -0
  9. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/pyproject.toml +4 -2
  10. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/.gitignore +0 -0
  11. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/LICENSE +0 -0
  12. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/compiling.py +0 -0
  13. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/completion.py +0 -0
  14. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/device.py +0 -0
  15. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/importing.py +0 -0
  16. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/__init__.py +0 -0
  17. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/anthropic.py +0 -0
  18. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/anthropic_serve.py +0 -0
  19. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/huggingface_complete.py +0 -0
  20. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/huggingface_image.py +0 -0
  21. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/providers/huggingface_image_serve.py +0 -0
  22. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/py.typed +0 -0
  23. {cortexgrid_infer-0.2.4 → cortexgrid_infer-0.2.7}/cortexgrid_infer/requirements.py +0 -0
  24. {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
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 it comes from the model: the cache is
188
- pinned to `config.max_position_embeddings`. A prompt longer than that is
189
- truncated to it — keeping the end, since a chat template puts the turn to
190
- answer last — with a warning naming both lengths. Serving it instead would
191
- resize the cache and recompile the decode loop, which costs more than the
192
- generation itself.
193
-
194
- Neither asks to be configured, for the same reason the serve apps do not ask which
195
- dtype to load in. Both happen on CUDA and nowhere else — inductor is weakest off
196
- it, and a host outrunning its accelerator is a GPU problem to begin with.
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 it comes from the model: the cache is
162
- pinned to `config.max_position_embeddings`. A prompt longer than that is
163
- truncated to it — keeping the end, since a chat template puts the turn to
164
- answer last — with a warning naming both lengths. Serving it instead would
165
- resize the cache and recompile the decode loop, which costs more than the
166
- generation itself.
167
-
168
- Neither asks to be configured, for the same reason the serve apps do not ask which
169
- dtype to load in. Both happen on CUDA and nowhere else — inductor is weakest off
170
- it, and a host outrunning its accelerator is a GPU problem to begin with.
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
- # Used only by a model whose config states no context length of its own.
33
- FALLBACK_MAX_INPUT_TOKENS = 4096
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
- self._max_input_tokens = getattr(
59
- self._model.config, "max_position_embeddings", FALLBACK_MAX_INPUT_TOKENS
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._max_input_tokens
79
+ config.max_cache_len = self._max_total_tokens
70
80
  config.compile_config = CompileConfig(mode=compiling.Mode.GRAPHED)
71
81
 
72
- def _truncate(self, inputs: Any) -> Any:
73
- """Cut an over-long prompt to what the model handles, keeping its end."""
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 <= self._max_input_tokens:
95
+ if length <= limit:
76
96
  return inputs
77
97
  log.warning(
78
- "truncating prompt from %d to %d tokens, the model's context",
79
- length, self._max_input_tokens,
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[..., -self._max_input_tokens :]
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 = body.get("max_new_tokens", 16 * 1024)
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(self._tokenizer(prompt, return_tensors="pt")).to(
103
- self._model.device
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"
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]