FRIDA-Decisions answers structured questions about a text — pick an option, place it on a scale, say yes or no, rank candidates — and returns probabilities, not generated text. All questions about one text, and all their options, are scored together by the FRIDA encoder in packed sequences — the text is never re-encoded per option, and there is no decoding, no output tokens, no answer parsing.
- Four question types in one request:
choice,score,noul(yes/no) andranking. - Packed, not repeated. Questions and options sit next to one copy of the text; a typical request is a single encoder call, and a 243-intent catalog is still one request.
- Order-independent by construction. Options never attend to each other, so adding, removing or reordering options does not change any other option's margin.
- Exact state cache. The text is encoded once and its keys/values are reused across rows and repeated requests — the same margins, less compute.
- GPU or CPU. PyTorch on GPU or CPU, an int8 ONNX model for CPU that does not need torch, MLX on Apple Silicon, or a vLLM server.
Developed by УЭСМО, SberAI.
# PyTorch backend (GPU or CPU)
pip install "frida-decisions[torch] @ git+https://github.com/ai-forever/FRIDA-Decisions@v0.4.0"
# int8 ONNX backend for CPU, without torch
pip install "frida-decisions[onnx] @ git+https://github.com/ai-forever/FRIDA-Decisions@v0.4.0"
# MLX backend for Apple Silicon, without torch
pip install "frida-decisions[mlx] @ git+https://github.com/ai-forever/FRIDA-Decisions@v0.4.0"
# vLLM server (GPU, Linux)
pip install "frida-decisions[vllm] @ git+https://github.com/ai-forever/FRIDA-Decisions@v0.4.0"The core package needs only numpy, tokenizers, safetensors and huggingface_hub; each backend comes with its extra ([torch], [onnx], [mlx], [vllm]). Python 3.10+. The weights are downloaded from the Hugging Face Hub on first use.
from frida_decisions import Judge
judge = Judge.from_pretrained("ai-forever/FRIDA-Decisions") # GPU if available, else CPU
response = judge({
"state": "Добрый день. Третий день не могу войти в личный кабинет: пишет, что пароль неверный, "
"а письмо для сброса не приходит. Из-за этого не могу скачать счёт, срок оплаты завтра.",
"questions": {
"team": {"type": "choice", "instructions": "В какую команду направить обращение?",
"criteria": {"auth": "доступ к аккаунту, вход, пароли",
"billing": "счета, оплата, возвраты",
"shipping": "доставка заказов"}},
"angry": {"type": "noul", "instructions": "Автор раздражён?"},
},
})
response["answers"]["team"]["choice"] # "auth"
response["answers"]["angry"]["noul"] # probability of "yes"Several independent requests (different texts) can go in one call; you get one response per request, in the same order, with the same answers as calling judge on each:
responses = judge.judge_batch([request_1, request_2, request_3])More in examples/quickstart.py and the notebook notebooks/quickstart.ipynb — run it in Colab:
Worked scenarios (routing, guardrails, retrieval, tool choice, and others) are indexed in examples/README.md.
A request is a state — a string or any JSON value (objects are rendered as compact, key-sorted JSON) — and named questions. Each question has a type, free-text instructions and, depending on the type, criteria. Every response carries answers (one per question), the raw margins of every option, and usage.
The answers below are real outputs of the released model (float32, CPU), trimmed to three decimals.
{"state": "Здравствуйте! Вчера оплатил заказ №5512 картой, деньги списались дважды. Верните, пожалуйста, лишнее списание.",
"questions": {"topic": {"type": "choice", "instructions": "Какая тема обращения?",
"criteria": {"billing": "оплата, списания, возвраты денег",
"delivery": "доставка и сроки получения заказа",
"account": "вход в аккаунт, пароль, личные данные",
"product": "качество и характеристики товара"}}}}{"answers": {"topic": {"type": "choice", "probabilities": {"billing": 0.999, "delivery": 0.001, "account": 0.0, "product": 0.001}, "confidence": 0.992, "choice": "billing"}}}An option is shown to the model as "<id>: <description>", so keep ids short and neutral and put the meaning in the description.
{"state": "Сервер с базой заказов не отвечает уже сорок минут, клиенты не могут оформить покупку, каждая минута — потерянные продажи.",
"questions": {"urgency": {"type": "score", "instructions": "Оцени срочность обращения по шкале ниже.",
"criteria": ["Очень низкая: работа не страдает, ответить можно когда угодно.",
"Низкая: неудобство небольшое, ответить можно на этой неделе.",
"Средняя: работа затруднена, но обходной путь есть.",
"Высокая: работа заблокирована, обходного пути нет.",
"Критическая: потери растут с каждым часом."]}}}{"answers": {"urgency": {"type": "score", "probabilities": {"0": 0.0, "1": 0.0, "2": 0.0, "3": 0.086, "4": 0.914}, "confidence": 0.816, "score": 3.913, "legend": {"0": "Очень низкая: …", "...": "…", "4": "Критическая: …"}}}}score is the expected level (0 … N−1) under probabilities; legend maps levels back to their descriptions.
{"state": "Курьер опоздал на два часа и даже не позвонил. Больше заказывать у вас не буду.",
"questions": {"complaint": {"type": "noul", "instructions": "Это жалоба?"}}}{"answers": {"complaint": {"type": "noul", "noul": 0.674}}}noul is the probability of "yes". criteria is optional: {"true": "...", "false": "..."} replaces the default wording of either side.
{"state": "Как удалить накипь из чайника?",
"questions": {"best": {"type": "ranking", "instructions": "Какой фрагмент лучше всего отвечает на вопрос?",
"criteria": {"p1": "Налейте в чайник воду с двумя ложками лимонной кислоты, вскипятите и оставьте на час, затем промойте.",
"p2": "Электрический чайник нельзя погружать в воду целиком: это опасно для нагревательного элемента.",
"p3": "Чай лучше заваривать водой температурой 85–95 градусов, а не крутым кипятком.",
"p4": "Уксус тоже растворяет накипь: смешайте его с водой один к одному, прокипятите и тщательно сполосните."}}}}{"answers": {"best": {"type": "ranking", "ranking": ["p4", "p1", "p2", "p3"], "scores": {"p1": 4.408, "p2": 0.167, "p3": -0.065, "p4": 5.851}, "probabilities": {"p1": 0.19, "p2": 0.003, "p3": 0.002, "p4": 0.805}, "confidence": 0.625}}}A ranking candidate is content (a passage, an agent action, a document), not a labelled option: its id is never shown to the model. criteria may also be a plain list; ids are then "0", "1", … scores are the raw margins — use them for top-k or a threshold (see Filtering ranking candidates by a threshold); probabilities split one unit among the candidates.
probabilities cannot filter: they split one unit among the candidates, so with 250 candidates a confident winner still reads 0.09. scores can — each margin is computed independently of the other candidates, and adding or removing a candidate leaves the rest untouched. What they are not is calibrated: the head is trained with a listwise softmax, which depends only on differences inside one list, so nothing pins the level of a particular request's margins. On the relevance task of razvilka (50 queries, 5 candidates, one relevant and four annotated negatives) that level runs from −1.88 to +10.84 with sd 2.32, against a median gap of +4.92 between the relevant candidate and the best irrelevant one. Everything below is tools/ranking_threshold.py; the held-out column fits the threshold on four fifths of the queries and scores it on the rest.
| threshold on | AUC (95 % CI) | best single threshold | held-out F1 |
|---|---|---|---|
scores as they are |
0.923 (0.882–0.959) | +6.76 → F1 0.769 | 0.752 |
scores minus a fixed anchor candidate |
0.929 (0.890–0.964) | +7.18 → F1 0.774 | 0.732 |
scores minus the median of the request's own candidates |
0.951 (0.910–0.982) | +4.40 → F1 0.835 | 0.796 |
scores minus the mean of the request's own candidates |
0.972 (0.943–0.992) | +3.52 → F1 0.882 | 0.855 |
So subtract the request's own list, not a constant:
answer = judge(request)["answers"]["best"]
scores = answer["scores"]
level = sum(scores.values()) / len(scores)
relevant = [key for key, score in scores.items() if score - level >= 3.5]Adding an anchor candidate — one extra candidate whose text says it is not an answer — is the obvious thing to try and it does not work: its own margin barely moves with the request (median +0.63, sd 0.65, correlation with the request's level +0.22), so subtracting it removes noise rather than the offset. The list does carry the offset, because most of a retriever's top-k is irrelevant and their margins are where that request's "irrelevant" sits.
Which is also what the recipe assumes, and what it cannot do:
- The threshold is not a constant. +3.5 is fitted on lists of five with one relevant candidate. Keep it and shorten the list and recall falls away — on lists of three, F1 0.837; on lists of two, 0.689 — because the relevant candidate is itself part of the mean and drags it up. Refitted, the same lists reach 0.917 and 0.953 at +1.62 and −0.09. Re-fit on lists the shape of yours.
- It cannot say that nothing is relevant. Drop the relevant candidate and centre the four negatives among themselves, and 5.5 % of them still clear +3.52 (the raw threshold +6.76 leaves 7.0 %): centring ranks the best of the list, it does not recognise relevance. For an absolute per-candidate probability, ask
noulper candidate — it is trained with a pairwise objective and returns a probability in 0…1. - Lists with several relevant candidates are not measured here. This task has exactly one, so these numbers describe a pool that is 20 % relevant. 50 queries is also a small set: the intervals above are bootstrap over queries, and the ones for the raw and the centred threshold do overlap at the edges.
Invalid requests raise frida_decisions.RequestError; error.payload() gives {"error": {"type", "message", "field"}}.
The model is FRIDA (a T5 encoder) with a linear head: a candidate's margin is the head applied to the mean of its own token states. Instead of encoding [text][question][option] once per option, a request is packed into one sequence with a block mask:
tokens: [ state ......... ][ question 1 ][ opt A ][ opt B ][ opt C ][ question 2 ][ yes ][ no ]
positions: 0 ............ S S ...... S+q S+q.. S+q.. S+q.. S ...... S+r S+r.. S+r..
state -> sees the state only
question -> sees the state and itself
option -> sees the state, its own question and itself (never another option)
Positions restart after each question, so every option sits exactly where it would sit if it were scored alone; the mask makes the rest identical too. Large catalogs are split into rows of at most 16 options / 1,024 tokens — exactly, since options are independent.
Because the state only ever attends to itself, its per-layer keys and values do not depend on the questions. The torch backend encodes the state once and runs the question/option rows against the cached keys/values. This is exact (float32 drift ≈ 6.7e-06), and it kicks in automatically when a request needs more than one row or the same text was seen recently.
For the 243-intent example in examples/data/intent_catalog.json the encoder reads 97,685 tokens if every intent gets its own sequence, 9,836 (16 rows) when packed, and 4,871 with the state cache.
| backend | install | where | notes |
|---|---|---|---|
Judge (PyTorch) |
[torch] |
GPU (bf16) or CPU (fp32) | packing + exact state cache |
OnnxJudge (ONNX Runtime) |
[onnx] |
CPU | int8, packing via graph inputs, no state cache |
MlxJudge (MLX) |
[mlx] |
Apple Silicon (fp32 or bf16) | packing + exact state cache, no torch |
| vLLM server | [vllm] |
GPU (bf16), Linux | packing + state cache through vLLM's prefix cache, continuous batching across users |
AsyncJudge |
[torch], [onnx] or [mlx] |
wherever Judge / OnnxJudge / MlxJudge runs |
await judge.judge(request); requests awaited together share judge_batch calls |
VllmJudge |
[vllm] |
GPU (bf16), Linux | await judge.judge(request) on the vLLM engine in your process: the server's batching and state cache, no HTTP |
The ONNX model is for CPU only. Its weights are int8 (one scale per output channel) and its activations are quantised to int8 on the fly, with one scale per token, while the decision head stays in float32. Quantisation changes the margins: on the 122 decisions of the test set, the ONNX model agrees with PyTorch float32 on 120, and the largest margin difference is 0.65. Where every decision matters, use the PyTorch backend.
from frida_decisions import OnnxJudge
judge = OnnxJudge.from_pretrained("ai-forever/FRIDA-Decisions", threads=8)
judge(request) # same request and response formatMlxJudge runs the encoder in MLX on the Apple GPU, without torch, with the same packing and exact state cache as Judge. The encoder runs in float32 by default; dtype=mlx.core.bfloat16 halves its memory, and the decision head stays in float32 either way. Contributed by @akolotov.
from frida_decisions import MlxJudge
judge = MlxJudge.from_pretrained("ai-forever/FRIDA-Decisions") # float32
judge(request) # same request and response formatOn razvilka (Apple M4 Pro, tools/mlx_check.py) MLX in float32 makes the same 735 decisions as PyTorch float32 on CPU; in bfloat16 it makes 733 of them, and its margin error against float32 (median 0.018, p99 0.108) is that of bfloat16 in PyTorch. A request with a ≈400-token state and 3 questions takes 156 ms in float32 and 139 ms in bfloat16.
rows_per_forward(default 1) caps the packed rows per encoder call, and with it the attention memory;Noneruns all rows at once.state_cache_mb(default 512) bounds the state cache, 0 turns it off;compile_encoder=Falseturns offmx.compile.AsyncJudge.from_pretrained(..., backend="mlx")wraps it like the other judges.- PyTorch also runs on a Mac:
Judge.from_pretrained(..., device="mps")(bfloat16) makes the same 735 decisions on razvilka. MLX in bfloat16 takes 17–19 % less time on the requests above (113 against 139 ms with 1 question, 139 against 168 ms with 3; one run) and does not need torch.
More in examples/mlx_quickstart.py.
[vllm] installs a vLLM plugin: the model, its attention (the packed block mask and FRIDA's relative positions, which vLLM's own attention kernels do not support) and the request front end. vLLM finds it on start; there is nothing to import.
vllm serve ai-forever/FRIDA-Decisions \
--hf-overrides '{"architectures": ["FridaDecisionsModel"]}' \
--io-processor-plugin frida_decisions \
--no-enable-chunked-prefill --enforce-eager --max-model-len 2048In Docker (Linux, or Windows with WSL2); the vLLM image has no git, so the package comes as the release archive:
docker run --gpus all --ipc=host -p 8000:8000 --entrypoint bash vllm/vllm-openai:v0.29.0 -c \
'pip install "frida-decisions[vllm] @ https://github.com/ai-forever/FRIDA-Decisions/archive/refs/tags/v0.4.0.tar.gz" && exec vllm serve ai-forever/FRIDA-Decisions --hf-overrides "{\"architectures\": [\"FridaDecisionsModel\"]}" --io-processor-plugin frida_decisions --no-enable-chunked-prefill --enforce-eager --max-model-len 2048'A request is the same JSON as for Judge, under data; the response data is what Judge returns:
import httpx
r = httpx.post("http://localhost:8000/pooling", json={"model": "ai-forever/FRIDA-Decisions", "data": request})
r.json()["data"]["answers"]A fuller client — a follow-up question answered from the cache, the 243-intent catalog, a burst of concurrent requests — is examples/vllm_client.py (standard library only).
- GPU memory. vLLM takes 90 % of the GPU by default; on a card that also drives a display pass
--gpu-memory-utilization(0.45 on an 8 GB card leaves room for the desktop and holds about 9,700 tokens of cache). - Required flags. The attention is bidirectional, so a text split across scheduler steps would be encoded without seeing its own end: the server refuses to start with chunked prefill on.
--kv-cache-dtypemust stayauto, because a cached text is reused bit for bit.--enforce-eageris expected (the attention is plain PyTorch). - State cache. Prefix caching is on by default. A text the server has already read is not encoded again, whether the next request comes from the same user or another one;
usage.cached_tokensshows it. Each row starts with a hash of the whole text, so a cached block is reused only for the same text, never for another text that starts the same way.--no-enable-prefix-cachingturns the cache off; passcache_saltin the request body to share cached texts only within one tenant. - Limits. The packing limits come from
decisions_config.json; the state is cut at 384 tokens, as inJudge(FRIDA_DECISIONS_STATE_MAXin the server's environment changes it). - Raw token ids. Clients that tokenize themselves can send rows to
POST /poolingwith"task": "classify"and"input": [[ids], ...]and get each row's margins;frida_decisions.vllm_backend.rows.build_rowsbuilds the rows. A row that breaks the format, or whose text hash is wrong, is refused with HTTP 400 and does not affect other requests. - Version. Tested with vLLM 0.29.0. The plugin uses vLLM internals (attention backends, poolers) that change between releases; with another version the server logs a warning, and
tools/vllm_parity.pychecks a running server againstJudge. - One difference from
Judge. Special-token strings inside the text (a literal<s>or</s>) are tokenized as plain text, because the plugin uses those tokens to mark the row layout;usage.special_tokens_splitlists where that happened.
AsyncJudge and VllmJudge give async def judge(request) -> response with the request and response of Judge:
import asyncio
from frida_decisions import AsyncJudge, VllmJudge
async def main(requests):
# PyTorch (or backend="onnx"): any OS, GPU or CPU
async with AsyncJudge.from_pretrained("ai-forever/FRIDA-Decisions") as judge:
response = await judge.judge(requests[0])
# the vLLM engine inside this process (Linux, CUDA GPU), no server
async with VllmJudge.from_pretrained("ai-forever/FRIDA-Decisions", gpu_memory_utilization=0.45) as judge:
responses = await asyncio.gather(*(judge.judge(r) for r in requests))More in examples/async_judge.py.
AsyncJudgeruns the model in one worker thread, so the event loop stays free while it computes. Requests awaited at the same time are scored together: while one batch runs, the next queues up and goes tojudge_batchin one call (up tomax_batch, 8 by default). It wraps any judge:AsyncJudge(Judge.from_pretrained(...)).VllmJudgestarts the vLLM engine (in a process vLLM manages) with the flags the model needs and submits each request's rows to it. Requests from all coroutines are batched by vLLM's scheduler, and a text the engine has already read comes from its prefix cache; passcache_salt=tojudge()to share cached texts only within one tenant. Cancelling an awaitingjudge()aborts its rows in the engine. Stop it withclose()orasync with.- Which one depends on the load (RTX 5060 Ti, in a vLLM 0.29 container, both judges with default settings,
VllmJudgewith 45 % of the GPU; medians of alternating runs,tools/async_bench.py): on a burst of 256 short tickets of one length (≈190 tokens)AsyncJudgeover PyTorch answers ≈187 requests/s against ≈126 forVllmJudge; on razvilka's mix of lengths with 32 requests in flight, ≈58 against ≈69. Likely, but not profiled: packed batches suit equal lengths and pad mixed ones, while vLLM schedules rows of any length.VllmJudgealso keeps the texts it has read in vLLM's prefix cache. Both raiseRequestErrorfor an invalid request, asJudgedoes.
The checks live in tests/ and tools/ (results are written locally to tests/_results/, which is not committed); CPU and float32 unless noted. The 36-request set is the 9 requests in tests/ plus 27 longer demo requests (routing, tool choice, moderation, support triage, a 243-intent catalog, 100-passage ranking); of those, only the intent catalog ships in examples/data/.
| check | result |
|---|---|
| exported model vs the training-time implementation (unmerged LoRA adapter) | 122/122 same decisions on 36 requests, max margin drift 3.8e-06 |
| released bf16 weights (run in float32) vs the same | 122/122 same decisions, max margin drift 0.045 |
| state cache vs packed rows | same decisions on 36 requests, max margin drift 6.7e-06 |
| packed rows vs one sequence per option | max margin drift 5.7e-06 on 9 requests |
| ONNX int8 vs PyTorch float32 | 120/122 same decisions (98.4%), max margin drift 0.65 |
| ONNX int8 (CPU) vs PyTorch bf16 (GPU) on razvilka | 726/735 same decisions; accuracy 0.891 vs 0.893 (tools/release_eval.py) |
| CPU latency, one request: 384-token state, 3 questions (8 options), 6 threads, background load | PyTorch fp32 2.28 s, ONNX int8 0.88 s (about 2.5x), tools/cpu_latency_ab.py |
| GPU latency, one request: ≈400-token state, RTX 5060 Ti, bf16 | 28.2 ms with 1 question, 34.0 ms with 3 questions (tools/release_eval.py) |
| peak GPU memory allocated by PyTorch over the razvilka run (without the CUDA context) | 1.8 GiB |
| vLLM backend arithmetic (CPU, float32, simulated paged cache) vs PyTorch float32 | same decisions on 9 requests, cold and with the state from the cache, max margin drift 5.2e-06 (tests/test_vllm_backend.py) |
| vLLM server (bf16, GPU) vs PyTorch bf16 (GPU) on razvilka | accuracy 0.890 vs 0.893 (654 vs 656 of 735, tools/release_eval.py --vllm): the 2 items where they differ are near-ties in float32 (top-two margin gaps 0.053 and 0.008), and PyTorch bf16 lands on the float32 side. The margin error from float32 is the same size for both over all 3,762 options — median 0.017 / 0.017, p95 0.066 / 0.067, p99 0.108 / 0.102 (vLLM / PyTorch) — and the same with the text read from the cache |
| vLLM server throughput, requests in flight | razvilka (735 requests, ≈260 tokens each), 8 in flight: 60.8 requests/s; short tickets (640 requests, ≈190 tokens, examples/vllm_client.py --burst 640): 81 with 8 in flight, 90 with 16 |
| vLLM server latency, one client over HTTP: ≈400-token state, RTX 5060 Ti, bf16 | 40 ms with 1 question, 44 ms with 3; 34 / 30 ms when the text is already cached |
async API, RTX 5060 Ti, bf16, in a vLLM 0.29 container, default settings, VllmJudge with 45 % of the GPU, every run with a cold cache (tools/async_bench.py) |
256 short tickets (≈190 tokens) awaited at once: AsyncJudge over PyTorch 187 requests/s (186–189 over 5 runs), VllmJudge 126 (125–127); razvilka, 735 items, 32 in flight: 58 (58–58 over 3 runs) and 69 (69–69). Both make the same decisions as PyTorch float32 on the test requests (15/15, tools/async_check.py) |
MLX vs PyTorch float32 (CPU) on razvilka, Apple M4 Pro, MLX 0.32.3 (tools/mlx_check.py) |
float32: 735/735 same decisions, max margin drift 2.9e-05; bfloat16: 733/735, accuracy 0.891 vs 0.894, margin error median 0.018, p99 0.108 over 3,762 options (PyTorch bf16 on a GPU: 0.017 / 0.102); PyTorch on MPS, bfloat16: 735/735, margin error 0.017 / 0.108. MLX state cache vs packed rows on the intent catalog: max drift 1.2e-05 (float32). Under AsyncJudge (worker thread, 14 requests batched), margins within 1.2e-05 of direct calls; a batched call scores the catalog through packed rows, not the state cache |
latency on Apple M4 Pro, one request: ≈400-token state (state_max=512), median of 30 after warm-up (tools/mlx_check.py) |
MLX float32 126 ms with 1 question, 156 ms with 3; MLX bfloat16 113 / 139 ms; PyTorch on MPS 139 / 168 ms in bfloat16, 152 / 186 ms in float32 |
peak MLX memory over the whole run above (load, razvilka, intent catalog, latency), rows_per_forward=1 |
4.6 GiB in float32, 2.2 GiB in bfloat16 |
a single threshold on ranking margins, relevance task of razvilka (50 queries, 5 candidates, tools/ranking_threshold.py) |
pooled AUC 0.923 (bootstrap 95 % 0.882-0.959), best single threshold +6.76 -> F1 0.769, held out 0.752. Minus the mean of the request's own candidates: 0.972 (0.943-0.992), +3.52 -> 0.882, held out 0.855; minus the median 0.951 / 0.835 / 0.796; minus a fixed anchor candidate 0.929 / 0.774 / 0.732. The anchor's own margin has sd 0.65 and correlates +0.22 with the request's level (sd 2.32), which is why it removes nothing. The fitted threshold does not carry to shorter lists (five candidates 0.882, three 0.837, two 0.689) and does not recognise a list with nothing relevant (5.5 % of the negatives clear it) |
| accuracy on razvilka (735 items) | 0.893 (PyTorch bf16, GPU), see the model card |
GPU parity with the CPU path was smoke-tested on 15 decisions (tools/gpu_parity.py, all equal); the razvilka run above is the larger check.
- State: up to 512 tokens is the recommended range — the range the model was trained on.
Judge.from_pretrained(..., state_max=384)sets where the state is cut (default 384); longer states are cut from the end, andusage.state_truncatedtells you when that happened. - Instructions: up to 96 tokens per question, including a short type-specific suffix the model was trained with.
- Options: up to 256 tokens each. The number of options is limited only by time and memory; rows hold up to 16 options / 1,024 tokens and are split automatically.
- Language: FRIDA is a Russian–English encoder, and the built-in instruction suffixes are in Russian.
The packing limits ship with the weights in decisions_config.json and are read by every backend.
config.json T5 encoder config
model.safetensors encoder weights (bfloat16), LoRA already merged
head.safetensors decision head, float32: weight (1, 1536), bias (1,)
tokenizer.json, tokenizer_config.json
decisions_config.json packing limits, instruction suffixes, relative-position settings
onnx/model_int8_pertoken.onnx int8 graph for OnnxJudge (1.17 GiB)
tools/export_model.py builds this folder from a FRIDA LoRA adapter (merge W += B·A·α/r, no peft needed at inference), and tools/export_onnx.py adds the int8 ONNX graph.
pip install -e ".[dev]" # torch, transformers, onnx, onnxruntime, pytest
python tools/export_model.py --checkpoint <adapter folder> --out _export/FRIDA-Decisions
python tools/export_onnx.py --model-dir _export/FRIDA-Decisions
pytest -sModel tests are skipped when no exported folder is present (FD_MODEL_DIR). The MLX tests (tests/test_mlx*.py) need pip install -e ".[dev,mlx]" and run on Apple Silicon; tools/mlx_check.py runs the razvilka comparison and the latency above. The vLLM backend's own tests run on CPU without vLLM; a running server is checked against Judge with
python tools/vllm_parity.py --reference-only # float32 reference on CPU, before starting the server
python tools/vllm_parity.py --url http://127.0.0.1:8000MIT — see LICENSE. The base model FRIDA is © ai-forever and released under the MIT License.
@misc{frida_decisions_2026,
title = {FRIDA-Decisions},
author = {{УЭСМО, SberAI}},
year = {2026},
url = {https://github.com/ai-forever/FRIDA-Decisions}
}