# Run the judge locally with Laya

> The judge slot is a protocol, not a vendor. Since v0.6.27 a Laya checkpoint on disk — ModernBERT, Apache 2.0 — answers whether, choose and rate with no network, no secret and no cost per token, and the same block runs unchanged.

Published 2026-09-22 · https://synsema.org/blog/run-the-judge-locally-with-laya


A `judge` block asks typed questions about a state and gets calibrated probabilities back:
*is this true* (0.86), *which of these* (`billing`, and the distribution), *where on this scale*
(between `upset` and `furious`). Until **v0.6.27** that meant an account with TypeSafe and a call to
[Jev](/blog/how-to-use-jev-the-judge-block), or the `mock` provider, which gives you a shape with no
meaning.

The official binaries now ship a third backend. A **Laya** checkpoint on disk answers the same three
verbs with no network, no secret and no cost per token:

```
SYNSEMA_JUDGE_PROVIDER=laya
SYNSEMA_JUDGE_MODEL=/models/laya      # the checkpoint directory, or an org/repo already in the HF cache
```

Nothing else changes. The program does not know which backend answered, because the judge slot was
always a **protocol, not a vendor**.

## What Laya is

[Laya](https://huggingface.co/convaiinnovations/laya) is a non-autoregressive decision model from
Convai Innovations, **Apache 2.0**: a ModernBERT-large backbone (421M parameters) with a decision
head, which takes a state and typed questions and answers them in a single forward pass. It is the
same shape of thing as Jev — a System One model — from a different place, with weights you can put
on a disk you own.

Its three question types map onto the three verbs of the block:

| Synsema | Laya | You read |
|---|---|---|
| `whether "…"` | `noul` | `probability` |
| `choose "…" between {…} [or nothing]` | `choice` | `choice`, `probabilities`, `confidence` |
| `rate "…" across […]` | `score` | `score`, `level`, `levels`, `probabilities` |

`noul` is the upstream's name for a yes/no question; the language keeps `whether`, because adopting
a vendor's glossary is how a protocol turns into a dependency.

Nothing is downloaded for you: the checkpoint is ~843 MB and fetching it is a decision, like a
`.gguf`. If it is not there, the error says how to get it.

## The same block, with no key

This is the program from the Jev post — unchanged, byte for byte — with two environment variables
set differently:

```synsema
require judge

let ticket be {"subject": "Payouts failing", "messages": [
    {"from": "customer", "text": "Help! My payouts have been failing for 3 days and nobody
     answers. I want my money back NOW or I'm cancelling."}
]}

let v be judge ticket
    refund: whether "The customer is asking for money back"
    team:   choose "Which team should handle this?" between {
                "billing":   "Payments, invoicing, refunds",
                "technical": "Bugs, outages, integrations"
            } or nothing
    anger:  rate "How frustrated is the customer?" across {
                "calm":    "Polite, no complaint",
                "upset":   "Repeat contact, asks for a fix soon",
                "furious": "Caps, threats to cancel, demands immediate action"
            }

when confidence of v.team < 0.8
    print("gate: human")
otherwise
    print("gate: auto")
```

On a laptop with no GPU:

```
available: true
refund.probability: 0.8596
team.choice: billing
team.probabilities: {billing: 0.7621, technical: 0.0850, none: 0.1529}
team.confidence: 0.3595
anger.level: furious
anger.score: 1.7154
model: laya:/models/laya
usage: 288
gate: human
```

Read the fourth line from the bottom twice. The judge picked `billing` and it is right, but the mass
is spread — 0.36 of confidence — so the gate you already wrote sends the ticket to a person. That is
the whole reason to want a probability instead of a string, and it works identically against a
checkpoint in your own directory.

Five questions in two blocks took **7.9 s** of wall clock, model load included, and a single
question takes **2.5 s** end to end in a fresh process. Three runs gave identical numbers.
`usage` still counts input tokens — there is simply no bill behind them.

## `decide` too, with no network at all

If your program already uses `decide between […] given x`, `SYNSEMA_JUDGE_DECIDE=1` routes every
`decide` in the process through the judge as a calibrated `choose` — one of your options byte for
byte, no normalisation and no retry. With `laya` behind it, that means a decision that never leaves
the machine:

```
$ synsema run --cap-set judge,llm,stdout triage.syn
decide: refund
model: laya:/models/laya
available: true
```

No `net` in the capability set. Nothing to deny, because there is nothing to call.

## What is verified, and what is not

The sequence a System One model sees is where an error does **not** fail loudly: the markers still
run, the model simply scores something else, and the answer looks exactly as plausible as a right
one. So it is the part that gets a permanent test.

```
[CLS] <type> question: <instruction> [SEP] [MASK] option0 [MASK] option1 … [SEP] <state> [SEP]
```

The `[MASK]`s are **markers** — the model scores each position, and that score is the preference for
that option — so the order and the exact position matter. Against the reference implementation with
the real tokenizer: **96 identical tokens, markers at [12, 22, 32, 39]**. That rules out everything
we wrote around the model: the tokenizer, the rendering of options, the serialisation of the state,
the budget trimming.

What is **not** closed, and it should be said out loud: on the case published in the upstream README
we get the same argmax (`billing`) with **confidence 0.864** where the README reports 0.94. Our
arithmetic is correct given the distribution we observe, and the input tokens are identical, so the
difference is in the numbers coming out of the encoder, or the published value is not comparable
(another version of the checkpoint, or another checkpoint served through their Router). Settling it
requires running the upstream implementation, which needs PyTorch.

Which brings up the thing to do before you move a threshold from one backend to the other:
**re-measure it**. A 0.9 gate is a number tuned against a specific model. Jev and Laya are different
models; the shape of the answer is the same, the calibration is not necessarily.

## The other limits, in one place

- **512 tokens of context** (read from the checkpoint's own config). The prefix — question type,
  instruction, option markers — gets up to 192, and the state is truncated to what is left. A long
  ticket gets its tail cut, so put the part that matters first, or judge a field instead of the
  whole object.
- **CPU, in seconds, not milliseconds.** The model card's ~33 ms is not what a laptop does. Load is
  paid once per process; under `serve` the requests after the first reuse it.
- **It runs in `f32` although the checkpoint is `f16`**, because on CPU half precision is emulated
  and comes out slower. That costs ~1.7 GB of RAM with the model loaded.
- **The act/escalate head is deliberately not implemented.** It is a parallel branch that does not
  touch the logits — the answer is identical with or without it — and the `judge` contract does not
  expose an action probability, so it would be arithmetic nobody can read.

## What it changes about being offline

Every `judge` answer carries `available`, and without a provider it comes back `available: false`,
`confidence: 0` and its main value `nothing` — never an invented number. That degradation is
deliberate and it stays. What changes in v0.6.27 is that **being offline is now a choice rather than
a fate**: a program with `require judge`, no secret and no network gets real calibrated probabilities
from a file on disk.

`synsema judge status` shows only what applies to the backend that is selected — with `laya`, the
checkpoint and nothing else:

```
Provider    laya                         (SYNSEMA_JUDGE_PROVIDER, environ)
Checkpoint  /models/laya                 (SYNSEMA_JUDGE_MODEL, environ)
Budget      (sin techo)                  (SYNSEMA_JUDGE_BUDGET, default)
decide      LLM (default)                (SYNSEMA_JUDGE_DECIDE)

Estado: ✅ VIVO — los bloques `judge` corren LOCAL contra el checkpoint: sin red, sin clave y sin
costo por token.
```

Exit 0 live, 1 offline, no network touched — so `synsema judge status && synsema serve app.syn` is
still a deploy gate. And `judge_model()` returns `laya:<checkpoint>`, so two runs with different
weights never look alike in a log.

## Which one to use

| | `typesafe` (Jev) | `laya` | `mock` |
|---|---|---|---|
| Where | the vendor's API | a checkpoint on your disk | nowhere |
| Needs | a key | ~843 MB and RAM | nothing |
| Answers | real judgments | real judgments | a shape with no meaning |
| Good for | production at scale, long states | air-gapped, on-prem, CI with real numbers, no vendor | tests |

`mock` and `laya` answer different questions. `mock` belongs in CI, where a checkpoint has no
business being. `laya` is for when you want a real judgment and cannot — or will not — send the state
anywhere.

The manual page is [Judge](https://synsema.dev/en/0.6.x/54-judge); the block itself, verb by verb, is
in [How to use Jev from Synsema](/blog/how-to-use-jev-the-judge-block); and the generative half of
the same release — a model in your process, architectures as files — is in
[A model you already have](/blog/local-inference-architectures-as-files).

