cortexgrid-infer 0.2.3__tar.gz → 0.2.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 (21) hide show
  1. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/PKG-INFO +26 -11
  2. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/README.md +25 -10
  3. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/providers/huggingface_complete_serve.py +64 -6
  4. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/pyproject.toml +1 -1
  5. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/.gitignore +0 -0
  6. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/LICENSE +0 -0
  7. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/__init__.py +0 -0
  8. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/compiling.py +0 -0
  9. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/completion.py +0 -0
  10. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/core.py +0 -0
  11. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/device.py +0 -0
  12. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/importing.py +0 -0
  13. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/providers/__init__.py +0 -0
  14. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/providers/anthropic.py +0 -0
  15. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/providers/anthropic_serve.py +0 -0
  16. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/providers/huggingface_complete.py +0 -0
  17. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/providers/huggingface_image.py +0 -0
  18. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/providers/huggingface_image_serve.py +0 -0
  19. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/py.typed +0 -0
  20. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.6}/cortexgrid_infer/requirements.py +0 -0
  21. {cortexgrid_infer-0.2.3 → cortexgrid_infer-0.2.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.2.3
3
+ Version: 0.2.6
4
4
  Summary: Deploy and interact with models across providers (Anthropic, HuggingFace LLM + diffusion)
5
5
  Project-URL: Homepage, https://github.com/robodatalab/cortexgrid-infer
6
6
  Project-URL: Repository, https://github.com/robodatalab/cortexgrid-infer
@@ -178,16 +178,31 @@ Both serve apps are tuned rather than defaulted:
178
178
  whole loop. The VAE is left alone: smallest win of the three, and the one whose
179
179
  shapes move most, especially with tiling on.
180
180
  - **`HuggingFaceCompletingDeployment`** does *not* compile the module. Decode has
181
- no stable shape until the KV cache is static, so it sets
182
- `cache_implementation="static"` and a `CompileConfig(mode=Mode.GRAPHED)`, and
183
- transformers compiles `generate` itself — capturing prefill and decode
184
- separately, which it is better placed to do. Compiling the module here as well
185
- would only compile the same forward twice. The cost is VRAM: a static cache is
186
- preallocated to the length asked for, where a dynamic one grows into it.
187
-
188
- Neither asks to be configured, for the same reason the serve apps do not ask which
189
- dtype to load in. Both happen on CUDA and nowhere else — inductor is weakest off
190
- it, and a host outrunning its accelerator is a GPU problem to begin with.
181
+ no stable shape of its own — the sequence grows a token per forward — so it
182
+ pins one: `cache_implementation="static"` with a fixed `max_cache_len`, plus a
183
+ `CompileConfig(mode=Mode.GRAPHED)`. transformers then compiles `generate`
184
+ itself, capturing prefill and decode separately, which it is better placed to
185
+ do; compiling the module here as well would compile the same forward twice.
186
+
187
+ The fixed length is the point, and how long it is decides what decode costs:
188
+ every token's attention reads the whole cache, filled or not. That is why it is
189
+ *not* pinned to the model's context — 32k slots of a Qwen2.5-3B KV cache is
190
+ 1.2 GB of keys and values re-read per generated token, several times the
191
+ traffic of the model's own weights, to make room for a prompt nobody sent. It
192
+ is pinned instead to the prompt and reply a caller plausibly sends: 4096 tokens,
193
+ or `max_total_tokens` on the model card, never past the model's own
194
+ `max_position_embeddings`. A reply takes at most half of it, so an
195
+ over-optimistic `max_new_tokens` cannot squeeze the prompt to nothing, and a
196
+ prompt that will not fit in the rest is truncated — keeping the end, since a
197
+ chat template puts the turn to answer last — with a warning naming both
198
+ lengths. Serving it whole would resize the cache and recompile the decode loop,
199
+ which costs more than the generation itself.
200
+
201
+ The image app asks to be configured for nothing, for the same reason neither asks
202
+ which dtype to load in; the completion app takes `max_total_tokens` alone, because
203
+ how much cache a caller needs is the one thing the model cannot tell it. Both
204
+ happen on CUDA and nowhere else — inductor is weakest off it, and a host
205
+ outrunning its accelerator is a GPU problem to begin with.
191
206
 
192
207
  Two things worth knowing:
193
208
 
@@ -152,16 +152,31 @@ Both serve apps are tuned rather than defaulted:
152
152
  whole loop. The VAE is left alone: smallest win of the three, and the one whose
153
153
  shapes move most, especially with tiling on.
154
154
  - **`HuggingFaceCompletingDeployment`** does *not* compile the module. Decode has
155
- no stable shape until the KV cache is static, so it sets
156
- `cache_implementation="static"` and a `CompileConfig(mode=Mode.GRAPHED)`, and
157
- transformers compiles `generate` itself — capturing prefill and decode
158
- separately, which it is better placed to do. Compiling the module here as well
159
- would only compile the same forward twice. The cost is VRAM: a static cache is
160
- preallocated to the length asked for, where a dynamic one grows into it.
161
-
162
- Neither asks to be configured, for the same reason the serve apps do not ask which
163
- dtype to load in. Both happen on CUDA and nowhere else — inductor is weakest off
164
- it, and a host outrunning its accelerator is a GPU problem to begin with.
155
+ no stable shape of its own — the sequence grows a token per forward — so it
156
+ pins one: `cache_implementation="static"` with a fixed `max_cache_len`, plus a
157
+ `CompileConfig(mode=Mode.GRAPHED)`. transformers then compiles `generate`
158
+ itself, capturing prefill and decode separately, which it is better placed to
159
+ do; compiling the module here as well would compile the same forward twice.
160
+
161
+ The fixed length is the point, and how long it is decides what decode costs:
162
+ every token's attention reads the whole cache, filled or not. That is why it is
163
+ *not* pinned to the model's context — 32k slots of a Qwen2.5-3B KV cache is
164
+ 1.2 GB of keys and values re-read per generated token, several times the
165
+ traffic of the model's own weights, to make room for a prompt nobody sent. It
166
+ is pinned instead to the prompt and reply a caller plausibly sends: 4096 tokens,
167
+ or `max_total_tokens` on the model card, never past the model's own
168
+ `max_position_embeddings`. A reply takes at most half of it, so an
169
+ over-optimistic `max_new_tokens` cannot squeeze the prompt to nothing, and a
170
+ prompt that will not fit in the rest is truncated — keeping the end, since a
171
+ chat template puts the turn to answer last — with a warning naming both
172
+ lengths. Serving it whole would resize the cache and recompile the decode loop,
173
+ which costs more than the generation itself.
174
+
175
+ The image app asks to be configured for nothing, for the same reason neither asks
176
+ which dtype to load in; the completion app takes `max_total_tokens` alone, because
177
+ how much cache a caller needs is the one thing the model cannot tell it. Both
178
+ happen on CUDA and nowhere else — inductor is weakest off it, and a host
179
+ outrunning its accelerator is a GPU problem to begin with.
165
180
 
166
181
  Two things worth knowing:
167
182
 
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import logging
6
7
  from collections.abc import AsyncIterator
7
8
  from threading import Thread
8
9
  from typing import Any
@@ -11,16 +12,32 @@ import cortexgrid
11
12
  from cortexgrid import serve
12
13
  from fastapi import FastAPI
13
14
  from fastapi.responses import StreamingResponse
14
- from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
15
+ from transformers import (
16
+ AutoModelForCausalLM,
17
+ AutoTokenizer,
18
+ CompileConfig,
19
+ TextIteratorStreamer,
20
+ )
15
21
  import torch
16
22
 
17
23
  from cortexgrid_infer import compiling
18
24
  from cortexgrid_infer.device import detect_device
19
25
 
20
26
 
27
+ log = logging.getLogger(__name__)
28
+
21
29
  _app = FastAPI()
22
30
 
23
31
 
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
40
+
24
41
  _RESERVED_BODY_KEYS = {
25
42
  "messages",
26
43
  "tools",
@@ -44,16 +61,54 @@ class HuggingFaceCompletingDeployment:
44
61
  str(path), torch_dtype=torch.float16
45
62
  )
46
63
  self._model.to(self._device) # type: ignore
47
- # Decode's shapes move as the sequence grows, so fuse without capturing.
48
- self._compiled = compiling.compile_module(
49
- self._model, self._device, compiling.Mode.FUSED, label="model"
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
50
70
  )
71
+ # A static cache of fixed length is what makes decode compilable: it
72
+ # pins the shape every forward sees, so the loop is captured once
73
+ # instead of recompiled per token. transformers then compiles `generate`
74
+ # itself, which is why the module is not compiled here as well.
75
+ self._compiled = compiling.supported(self._device)
76
+ if self._compiled:
77
+ config = self._model.generation_config
78
+ config.cache_implementation = "static"
79
+ config.max_cache_len = self._max_total_tokens
80
+ config.compile_config = CompileConfig(mode=compiling.Mode.GRAPHED)
81
+
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."""
94
+ length = inputs["input_ids"].shape[-1]
95
+ if length <= limit:
96
+ return inputs
97
+ log.warning(
98
+ "truncating prompt from %d to %d tokens, the room this replica has",
99
+ length, limit,
100
+ )
101
+ for key, value in inputs.items():
102
+ inputs[key] = value[..., -limit:]
103
+ return inputs
51
104
 
52
105
  @_app.post("/complete")
53
106
  async def complete(self, body: dict[str, Any]) -> StreamingResponse:
54
107
  messages = body["messages"]
55
108
  tools = body.get("tools")
56
- 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
+ )
57
112
  temperature = body.get("temperature", 0.7)
58
113
  top_p = body.get("top_p", 0.9)
59
114
  top_k = body.get("top_k", 50)
@@ -66,7 +121,10 @@ class HuggingFaceCompletingDeployment:
66
121
  tokenize=False,
67
122
  add_generation_prompt=True,
68
123
  )
69
- inputs = self._tokenizer(prompt, return_tensors="pt").to(self._model.device)
124
+ inputs = self._truncate(
125
+ self._tokenizer(prompt, return_tensors="pt"),
126
+ self._max_total_tokens - max_new_tokens,
127
+ ).to(self._model.device)
70
128
 
71
129
  queue: asyncio.Queue[str | None] = asyncio.Queue()
72
130
  error: list[BaseException] = []
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cortexgrid-infer"
3
- version = "0.2.3"
3
+ version = "0.2.6"
4
4
  description = "Deploy and interact with models across providers (Anthropic, HuggingFace LLM + diffusion)"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"