fix(intern-decision-serve): code-review fixes
- a failure while building the response (a non-finite number included) is a 500 inside the envelope, never a 422 or a render crash outside it - an engine ValueError keeps its message but is released and raised unchained, like an OOM - the prompt is built (and the model's own validation run) before the forward - the row cap is counted before any ordering is built - /health reads a device name cached at load, so it makes no driver call off the inference thread
This commit is contained in:
@@ -124,6 +124,19 @@ def check_request(state: State, decisions: list[Decision], workload: str | None)
|
||||
raise bad(f"decision {d.id!r}: option ids must be unique")
|
||||
|
||||
|
||||
def ordering_count(d: Decision) -> int:
|
||||
"""How many orderings `d` asks, without building them (the row cap is checked first)."""
|
||||
n = len(d.options)
|
||||
if d.orderings == "none":
|
||||
return 1
|
||||
if d.orderings == "rotations":
|
||||
return n
|
||||
if n > MAX_OPTIONS_FOR_ALL:
|
||||
raise ApiError(422, "invalid_request", f"decision {d.id!r}: orderings 'all' allows at most "
|
||||
f"{MAX_OPTIONS_FOR_ALL} options ({n} given); use 'rotations'")
|
||||
return math.factorial(n)
|
||||
|
||||
|
||||
def ordering_perms(d: Decision) -> list[tuple[int, ...]]:
|
||||
"""Index permutations of the caller's options, the caller's own order first."""
|
||||
n = len(d.options)
|
||||
@@ -190,6 +203,19 @@ def row_result(slot: Slot, response: dict, sha: str, field: str, questions: int,
|
||||
"call": {"index": index, "field": field, "questions": questions}}
|
||||
|
||||
|
||||
def built(fn, *args):
|
||||
"""Building the response from the model's output is server-side work: any failure there is a 500
|
||||
scoring_failed, never a 422 the caller would read as their own bad input (review 2026-09-30)."""
|
||||
try:
|
||||
out = fn(*args)
|
||||
json.dumps(out, allow_nan=False) # a NaN/inf would otherwise crash rendering OUTSIDE the envelope
|
||||
return out
|
||||
except ScoringFailed:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise ScoringFailed(f"building the response failed: {type(exc).__name__}: {exc}") from None
|
||||
|
||||
|
||||
def combine(d: Decision, results: list[dict]) -> dict:
|
||||
"""semif-serve's averaging over log p (the model returns probabilities, not logits): the mean
|
||||
per option id, renormalised. The per-ordering results ride along unchanged."""
|
||||
@@ -259,12 +285,11 @@ def create_app(settings: Settings, engine: Any, executor: ThreadPoolExecutor | N
|
||||
|
||||
def planned(state: State, decisions: list[Decision], workload: str | None) -> list[list[Slot]]:
|
||||
check_request(state, decisions, workload)
|
||||
waves = plan_waves(decisions)
|
||||
rows = sum(map(len, waves))
|
||||
rows = sum(map(ordering_count, decisions)) # counted before anything is built
|
||||
if not 1 <= rows <= settings.max_decisions:
|
||||
raise ApiError(422, "invalid_request",
|
||||
f"this request scores {rows} rows; the limit is 1..{settings.max_decisions}")
|
||||
return waves
|
||||
return plan_waves(decisions)
|
||||
|
||||
def run(state: State, decisions: list[Decision], waves: list[list[Slot]]) -> tuple[list[dict], dict]:
|
||||
"""Every call of one request, back to back under the lock (INV-2); results in request order."""
|
||||
@@ -285,13 +310,13 @@ def create_app(settings: Settings, engine: Any, executor: ThreadPoolExecutor | N
|
||||
tokens.append(response["usage"]["input_tokens"])
|
||||
inference_ms += response["timing"]["inference_ms"]
|
||||
for f, s in zip(fields, chunk):
|
||||
by_slot[(s.decision, s.k)] = row_result(s, response, sha, f, len(chunk), index, model)
|
||||
by_slot[(s.decision, s.k)] = built(row_result, s, response, sha, f, len(chunk), index, model)
|
||||
out = []
|
||||
for i, d in enumerate(decisions):
|
||||
if d.orderings == "none":
|
||||
out.append(by_slot[(i, 0)])
|
||||
else:
|
||||
out.append(combine(d, [by_slot[(i, k)] for k in range(len(ordering_perms(d)))]))
|
||||
out.append(built(combine, d, [by_slot[(i, k)] for k in range(ordering_count(d))]))
|
||||
timing = {"total_seconds": time.perf_counter() - started, "batch_size": len(by_slot),
|
||||
"calls": len(sizes), "questions_per_call": sizes, "input_tokens": tokens,
|
||||
"inference_seconds": inference_ms / 1000}
|
||||
|
||||
@@ -80,6 +80,8 @@ class TorchEngine:
|
||||
self._torch, self._engine, self._inference, self._tokenizer = torch, engine, inference, tokenizer
|
||||
self.metadata, self._settings = metadata, settings
|
||||
self._release_above = release_above_bytes
|
||||
# Asked once, here, on the inference thread: /health then reads only allocator counters.
|
||||
self._device_name = torch.cuda.get_device_name(0) if settings.device == "cuda" else None
|
||||
|
||||
@classmethod
|
||||
def load(cls, settings: Settings, *, torch: Any = None,
|
||||
@@ -167,7 +169,7 @@ class TorchEngine:
|
||||
info = dict(self.metadata)
|
||||
if self._settings.device == "cuda":
|
||||
cuda = self._torch.cuda
|
||||
info["device_name"] = cuda.get_device_name(0)
|
||||
info["device_name"] = self._device_name
|
||||
info["allocated_gib"] = round(cuda.memory_allocated(0) / 2**30, 3)
|
||||
info["reserved_gib"] = round(cuda.memory_reserved(0) / 2**30, 3)
|
||||
if hasattr(cuda, "max_memory_reserved"):
|
||||
@@ -181,12 +183,15 @@ class TorchEngine:
|
||||
self._torch.cuda.empty_cache()
|
||||
|
||||
def predict(self, request: dict) -> tuple[dict, str]:
|
||||
"""One call: the model's own predict(), then the prompt hash (INV-8). Returns (response, sha)."""
|
||||
"""One call: the prompt hash (INV-8; this also runs the model's own validation before any GPU
|
||||
work), then the model's own predict(). Returns (response, sha)."""
|
||||
sha = hashlib.sha256(self._prompt_text(request).encode()).hexdigest()
|
||||
try:
|
||||
response = self._engine.predict(request)
|
||||
sha = hashlib.sha256(self._prompt_text(request).encode()).hexdigest()
|
||||
except ValueError:
|
||||
raise # the model's validation: raised before any GPU work
|
||||
except ValueError as exc:
|
||||
# Usually the token limit, found before the forward; but transformers raises ValueError
|
||||
# from inside a forward too. Either way, keep the message and release like any failure.
|
||||
failure, message = ValueError, str(exc)
|
||||
except self._torch.cuda.OutOfMemoryError as exc:
|
||||
failure, message = OutOfMemory, _first_line(exc) or "CUDA out of memory"
|
||||
except Exception as exc: # noqa: BLE001 — every other failure is released and reported below
|
||||
|
||||
Reference in New Issue
Block a user