skibare87 commited on
Commit
eafd2f1
·
verified ·
1 Parent(s): f2e726e

vision: images ride the DSpark speculative loop

Browse files
Files changed (1) hide show
  1. recipe/server.py +61 -10
recipe/server.py CHANGED
@@ -17,6 +17,9 @@ import time
17
  import threading
18
  import queue
19
  import uuid
 
 
 
20
 
21
  os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
22
  os.environ.setdefault("MASTER_PORT", "29500")
@@ -38,12 +41,13 @@ from torch.nn.attention import SDPBackend, sdpa_kernel
38
  # implement (NotImplementedError). It crashes the cache save. So we can't cache the dynamo frontend;
39
  # the ~200s warmup tracing stays. (AOTInductor would avoid dynamo entirely — separate effort.)
40
  from types import SimpleNamespace
41
- from typing import List, Optional
42
  import json as _json
 
43
  from fastapi import FastAPI
44
  from fastapi.responses import StreamingResponse
45
  from pydantic import BaseModel
46
- from transformers import AutoModelForCausalLM, AutoTokenizer, TorchAoConfig
47
  from torchao.quantization import (
48
  Float8DynamicActivationFloat8WeightConfig, Float8WeightOnlyConfig, PerRow)
49
  from torchao.quantization.quantize_.common.kernel_preference import KernelPreference
@@ -112,6 +116,10 @@ EV = FP8Gemma4DSparkEvaluator(0, _args)
112
  print(f"[dspark] TIMING model+quant load: {time.time()-_T0:.1f}s", flush=True)
113
  EV.confidence_head_recorder = None # metrics recorder is only started inside evaluate(); we bypass it
114
  TOK = EV.tokenizer
 
 
 
 
115
  STOP = resolve_stop_token_ids(EV.target_model, TOK) # gemma real eos set (e.g. [1,106,50]), not a guess
116
  _LOCK = threading.Lock()
117
 
@@ -156,7 +164,38 @@ app = FastAPI()
156
 
157
  class Msg(BaseModel):
158
  role: str
159
- content: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
 
162
  class ChatReq(BaseModel):
@@ -196,13 +235,25 @@ def _split_think(ids, thinking):
196
 
197
  @app.post("/v1/chat/completions")
198
  def chat(req: ChatReq):
199
- msgs = [{"role": m.role, "content": m.content} for m in req.messages]
200
  # thinking on by default; a `*-nothink` model alias (routed to this same backend) turns it off
201
  thinking = THINKING and not (req.model or "").endswith("-nothink")
202
- ids = TOK.apply_chat_template(
203
- msgs, add_generation_prompt=True, enable_thinking=thinking,
204
- return_tensors="pt", return_dict=True,
205
- )["input_ids"].to(EV.device)
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  cid = "chatcmpl-" + uuid.uuid4().hex[:12]
207
  created = int(time.time())
208
  model = req.model or MODEL_NAME
@@ -228,7 +279,7 @@ def chat(req: ChatReq):
228
  EV.args.temperature = temp
229
  with sdpa_kernel([SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH]), torch.no_grad():
230
  EV.generate_one_sample(
231
- input_ids=ids, stop_token_ids=STOP,
232
  stream_callback=lambda t: q.put(t[0].tolist()),
233
  )
234
  except Exception as e: # surface generation errors to the stream instead of hanging
@@ -271,7 +322,7 @@ def chat(req: ChatReq):
271
  EV.args.temperature = temp
272
  t = time.time()
273
  with sdpa_kernel([SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH]), torch.no_grad():
274
- res = EV.generate_one_sample(input_ids=ids, stop_token_ids=STOP)
275
  dt = time.time() - t
276
  gen = res.output_ids[0, res.num_input_tokens:].tolist()
277
  reasoning, content = _split_think(gen, thinking)
 
17
  import threading
18
  import queue
19
  import uuid
20
+ import io
21
+ import base64
22
+ import urllib.request
23
 
24
  os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
25
  os.environ.setdefault("MASTER_PORT", "29500")
 
41
  # implement (NotImplementedError). It crashes the cache save. So we can't cache the dynamo frontend;
42
  # the ~200s warmup tracing stays. (AOTInductor would avoid dynamo entirely — separate effort.)
43
  from types import SimpleNamespace
44
+ from typing import Any, List, Optional
45
  import json as _json
46
+ from PIL import Image
47
  from fastapi import FastAPI
48
  from fastapi.responses import StreamingResponse
49
  from pydantic import BaseModel
50
+ from transformers import AutoModelForCausalLM, AutoProcessor, AutoTokenizer, TorchAoConfig
51
  from torchao.quantization import (
52
  Float8DynamicActivationFloat8WeightConfig, Float8WeightOnlyConfig, PerRow)
53
  from torchao.quantization.quantize_.common.kernel_preference import KernelPreference
 
116
  print(f"[dspark] TIMING model+quant load: {time.time()-_T0:.1f}s", flush=True)
117
  EV.confidence_head_recorder = None # metrics recorder is only started inside evaluate(); we bypass it
118
  TOK = EV.tokenizer
119
+ # The multimodal processor (image + text). gemma-4-12B is a VLM; DSpark's target processes the image in
120
+ # the prefill (pixel_values), the KV cache carries it, and the draft speculates over image-aware hidden
121
+ # states — so vision rides the same speculative loop as text. Only loaded once here.
122
+ PROC = AutoProcessor.from_pretrained(TARGET)
123
  STOP = resolve_stop_token_ids(EV.target_model, TOK) # gemma real eos set (e.g. [1,106,50]), not a guess
124
  _LOCK = threading.Lock()
125
 
 
164
 
165
  class Msg(BaseModel):
166
  role: str
167
+ content: Any # str, or OpenAI multimodal list: [{"type":"text",...}, {"type":"image_url",...}]
168
+
169
+
170
+ def _load_image(url: str) -> Image.Image:
171
+ """Load a PIL image from a data: URI or an http(s) URL."""
172
+ if url.startswith("data:"):
173
+ return Image.open(io.BytesIO(base64.b64decode(url.split(",", 1)[1]))).convert("RGB")
174
+ return Image.open(io.BytesIO(urllib.request.urlopen(url, timeout=20).read())).convert("RGB")
175
+
176
+
177
+ def _build_chat(messages):
178
+ """Turn request messages into (chat-for-template, has_images). Accepts plain string content or an
179
+ OpenAI multimodal content list (text parts + image_url/image parts), which become gemma image parts."""
180
+ chat, has_images = [], False
181
+ for m in messages:
182
+ c = m.content
183
+ if isinstance(c, str):
184
+ chat.append({"role": m.role, "content": c})
185
+ continue
186
+ parts = []
187
+ for part in (c or []):
188
+ t = part.get("type") if isinstance(part, dict) else None
189
+ if t == "text":
190
+ parts.append({"type": "text", "text": part.get("text", "")})
191
+ elif t in ("image_url", "image"):
192
+ url = part.get("image_url", {}).get("url") if t == "image_url" else (part.get("image") or part.get("url"))
193
+ parts.append({"type": "image", "image": _load_image(url)})
194
+ has_images = True
195
+ elif isinstance(part, str):
196
+ parts.append({"type": "text", "text": part})
197
+ chat.append({"role": m.role, "content": parts})
198
+ return chat, has_images
199
 
200
 
201
  class ChatReq(BaseModel):
 
235
 
236
  @app.post("/v1/chat/completions")
237
  def chat(req: ChatReq):
 
238
  # thinking on by default; a `*-nothink` model alias (routed to this same backend) turns it off
239
  thinking = THINKING and not (req.model or "").endswith("-nothink")
240
+ chat_msgs, has_images = _build_chat(req.messages)
241
+ if has_images:
242
+ # multimodal: the processor emits input_ids + pixel_values/mm_token_type_ids/image_position_ids;
243
+ # the extra keys ride to the DSpark prefill (prefill_mm) so the target embeds the image and the
244
+ # draft speculates over image-aware hidden states — vision on the same speculative loop as text.
245
+ enc = PROC.apply_chat_template(
246
+ chat_msgs, add_generation_prompt=True, tokenize=True, return_dict=True,
247
+ return_tensors="pt", enable_thinking=thinking,
248
+ ).to(EV.device)
249
+ ids = enc["input_ids"]
250
+ prefill_mm = {k: enc[k] for k in enc if k not in ("input_ids", "attention_mask")}
251
+ else:
252
+ ids = TOK.apply_chat_template(
253
+ chat_msgs, add_generation_prompt=True, enable_thinking=thinking,
254
+ return_tensors="pt", return_dict=True,
255
+ )["input_ids"].to(EV.device)
256
+ prefill_mm = None
257
  cid = "chatcmpl-" + uuid.uuid4().hex[:12]
258
  created = int(time.time())
259
  model = req.model or MODEL_NAME
 
279
  EV.args.temperature = temp
280
  with sdpa_kernel([SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH]), torch.no_grad():
281
  EV.generate_one_sample(
282
+ input_ids=ids, stop_token_ids=STOP, prefill_mm=prefill_mm,
283
  stream_callback=lambda t: q.put(t[0].tolist()),
284
  )
285
  except Exception as e: # surface generation errors to the stream instead of hanging
 
322
  EV.args.temperature = temp
323
  t = time.time()
324
  with sdpa_kernel([SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH]), torch.no_grad():
325
+ res = EV.generate_one_sample(input_ids=ids, stop_token_ids=STOP, prefill_mm=prefill_mm)
326
  dt = time.time() - t
327
  gen = res.output_ids[0, res.num_input_tokens:].tolist()
328
  reasoning, content = _split_think(gen, thinking)