Pull requests / #1891

#1891 logit_bias: normalize a long list once per request (65,929 entries: +175 ms -> +24 ms)

open · @qni-live · 0 commentaires · Sur GitHub

Server & APIMulti-GPUAMD / HIPNVIDIA / CUDALinux

Description

Follow-up to #1646 (and #1627). One file changed (`serve/logit_bias.py`) plus tests. No engine change, no new option, the default path returns and raises exactly what it did.

**Result:** with a 65,929-entry list a request now costs **+24 ms instead of +175 ms** on a live 0.1.42 engine; the normalization alone drops from **204 ms to 8 ms** (five calls on a freshly parsed list). Same results, same errors, 12 tests (5 existing, 7 new).

## Why

A standing ban of a whole script is a long list: all Han, Kana and Hangul tokens of the vocabulary are 65,929 IDs (1.05 MB of JSON), sent with every request. On v0.1.42 it is normalized **five times per request**: `bias_api.normalize` in `_openai` (line 5217), `validate_request` at 5228 and again in `generate` (1569), and `engine_key` twice (`sampling_keys` runs for the cache key at 1253 and for the GEN line at 1577; each `engine_key` normalizes, sorts and joins a ~790 KB string). In isolation that is about 200 ms of Python per request, whatever the answer length.

Measured on a live 0.1.42 engine (2x RTX PRO 4500 layer split, Linux, 7,091-token prompt with prefix-cache hit, temperature 0, 9 runs per cell, medians): the list cost **+175 ms** at `max_tokens` 1, +173 ms at 50 and +180 ms at 400 (+10 to +18 % on a one-second agent turn). The engine's own decode rate is not affected (-1 %).

## What changed

`normalize()` and `engine_key()` keep their signatures and results. The module remembers the last normalized lists:

- the same object again in one request is answered from `_last`;
- an equal list in a later request is found by its compact JSON text (`vocab_size` is part of the key, so validation against the vocabulary is unchanged);
- only a list that normalized without an error is stored, so a bad list still raises on every call; lists under 64 entries skip the cache; at most 4 lists are kept.

The key is the JSON text, not the list: Python compares `[[7, False]]` and `[[7, 0]]` as equal, and a `==` key would turn a ban into "no bias". `normalize()` hands out a copy, so a caller cannot change a cached entry. `engine_key()` builds its string once per cached list.

## How it was checked

- `python -m unittest serve.test_logit_bias`: the 5 existing tests and 7 new ones pass (same answers as the uncached function for dict and pair-list forms and both vocabulary sizes; `False` is not `0` in either order; bad lists are never stored, including `True == 1`, a duplicate ID and NaN; vocabulary size is part of the key; the result is a copy; an equal list in a later request is a hit; the cache is bounded).
- The five calls of one request with a freshly parsed 65,929-entry list, Threadripper 7960X, CPython: **204 ms -> 8 ms**, same `engine_key` string (786,687 characters). A cold first request is 99 ms.
- On the live engine, the same measurement as above with this file in place: **+175 ms -> +24 ms** per request (65,929 IDs; 30,000 IDs +10 ms, 12,000 IDs +5 ms); the Han ban still works (a request for a Chinese translation returned 0 Han characters with the list, 6 without).

## What it leaves alone

- The wire format, the error messages and the HTTP 400 paths.
- The remaining ~24 ms are the JSON parse of the body and the compact JSON text used as the key. A standing ban in the config or a file read at start (so the list need not travel with each request at all) would remove them; that is a separate decision and this change does not preempt it.
- It assumes the parsed request body is not modified in place between the calls of one request, which nothing in `serve/` does.

Not tested: HIP and SYCL engines (Python only, no backend file touched), `--batch` (rejected before this code, unchanged).

Written with AI assistance (Claude); numbers reviewed by me.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Sur le site

Liens install, modèles, releases.