AntflyAntfly

Laya typed decisions

Run Laya checkpoints for typed classification decisions without text generation

Laya models answer classification questions without generating text. Antfly serves prepared Laya checkpoints through /ai/v1/decisions using the model-independent typed decision contract. They appear under deciders in model discovery. Standalone typed decisions use /decisions; extraction keeps ordinary classification alongside entities, relations, attributes and structured records.

Prepare a checkpoint from a local upstream download, or from a pinned Hugging Face revision:

uv run scripts/laya/prepare_laya.py convaiinnovations/laya \
  --revision <full-hugging-face-commit-sha> \
  --output ./models/extractors/laya
antfly standalone --models-dir ./models

The importer combines the encoder and decision configuration, preserves the weights and calibration, and copies the tokenizer. It refuses to overwrite an existing directory. Native support requires a ModernBERT-backed checkpoint with the Laya decision heads. The runtime validates tensor shapes before execution. Unprepared upstream checkpoints are not automatically converted by inference pull.

CUDA execution is available for Laya checkpoints with sequence lengths up to 512 and encoder head dimensions up to 128. It uses FP32 resident weights and bounded internal batches. The English checkpoint was validated on NVIDIA L4 using fatbin artifacts. Portable PTX requires a driver compatible with the CUDA toolkit used to generate it.

For decisions, submit the following to /ai/v1/decisions (or pass the same JSON to antfly_inference_decide_json / Rust Inference::decide):

{
  "model": "laya",
  "questions": [
    {
      "name": "tool",
      "type": "choice",
      "instructions": "Which tool should handle the request?",
      "choices": [
        {
          "value": "search",
          "description": "Find documents matching a topic"
        },
        {
          "value": "fetch_document",
          "description": "Retrieve a document with a known ID"
        },
        {
          "value": "no_tool",
          "description": "Respond without a tool"
        }
      ]
    },
    {
      "name": "urgency",
      "type": "score",
      "instructions": "How urgent is this request?",
      "levels": [
        {
          "label": "0",
          "description": "Routine"
        },
        {
          "label": "1",
          "description": "Soon"
        },
        {
          "label": "2",
          "description": "Immediate"
        }
      ]
    },
    {
      "name": "tool_needed",
      "type": "predicate",
      "instructions": "Answering requires retrieving external information."
    }
  ],
  "input": "Find the document about refunds."
}

The response has a named answers array: choice and its probability array, an expected zero-based score and labeled probability array, and a predicate true probability. Confidence and confidence_method remain model diagnostics. act_probability preserves the auxiliary action-head output for Laya checkpoints; OpenDecider-nano has no action head. Decisions do not execute a tool. Applications validate arguments and apply their action policy.

Batches use inputs with optional IDs; results preserve input order and IDs in data with aggregate token usage. See the decision guide for HTTP, CLI, embedded inference and SQL examples. The unreleased extraction decisions output and Boolean decision modes have been removed.

OpenDecider-nano#

OpenDecider-nano (Apache 2.0) is a 400M decision model on an Ettin encoder, a ModernBERT architecture, with Laya's per-option [MASK] scoring. Antfly serves it through the same pipeline and endpoints:

uv run scripts/laya/prepare_opendecider.py manjunathshiva/opendecider-nano \
  --revision <full-hugging-face-commit-sha> \
  --output ./models/extractors/opendecider-nano

The importer merges the encoder and the separate head into one checkpoint and sets laya.format: "opendecider". That format uses OpenDecider's prompt (question: and input: prefixes, its option text, "yes" before "no", no per-option token cap) and its marker head. It has no type embedding, head layers or action head, so decisions omit act_probability. It runs unpacked, with a 2,048-token budget; longer inputs are rejected, as for Laya.

Typed-decisions benchmark#

The community benchmark for typed-decision models is the LocalLLaMA/typed-decisions test split (400 cases, 2,000 decisions), scored per decision against the case's gold label. scripts/laya/prepare_laya_training_data.sh writes it as native records (td/eval.jsonl) and cases (td/eval-cases.jsonl).

antfly inference finetune eval laya ./models/extractors/opendecider-nano td/eval.jsonl \
  --backend metal --truncate-state --predictions nano.jsonl
uv run scripts/laya/typed_decisions_bench.py report nano.jsonl other.jsonl \
  --gold td/eval-cases.jsonl

--truncate-state cuts states that do not fit the checkpoint, as upstream Laya and OpenDecider do; 160 of the 2,000 decisions exceed Laya's 512 tokens. Serving never truncates. report prints accuracy per question type, Brier score, ECE and a paired bootstrap interval against the first file. typed_decisions_bench.py reference runs OpenDecider-nano's own PyTorch code for comparison. On Metal, Antfly matches it to 3e-6 in probability and reproduces its published 0.796.

Resident Metal inference#

Set ANTFLY_LAYA_METAL_RESIDENT=1 in the serving process environment before loading the model to keep the encoder, heads, and decision decoding on the GPU. Restart the process after changing it. Size model and request memory budgets for the checkpoint precision and padded batch size; once resident loading is attempted, failures are returned rather than retried on CPU.

Native finetuning#

antfly inference finetune train laya <job.json> trains the ModernBERT encoder, question-type embeddings, transformer head, and marker scorer in FP32. Choose cpu or metal explicitly. The default rlcd objective follows the upstream typed-decisions notebook: soft-target cross-entropy plus a centered Gaussian policy-gradient estimator with log, spherical, and ordinal ranked-probability rewards. soft_ce selects cross-entropy alone. This is full finetuning; it does not produce a LoRA adapter.

Prepare the base checkpoint with scripts/laya/prepare_laya.py as above. Supply separate train and evaluation JSONL files, with one decision per line:

{"id":"case-1/tool","group_id":"case-1","text":"Find the refund policy.","kind":"choice","instruction":"Which tool should handle the request?","labels":["search","fetch_document","no_tool"],"descriptions":["Find documents by topic","Retrieve a known document ID","Answer without a tool"],"target":[0.95,0.03,0.02]}

kind is choice, score, or noul. Targets are probabilities in label order and must sum to one; one-hot labels are also valid. Ordinal labels run from lowest to highest. Boolean labels must be false, then true. Descriptions are optional. Preprocessing shares the inference tokenizer and formatting, including explicit rejection of overlength text. No training examples are silently truncated or discarded.

To convert a JSONL export of LocalLLaMA/typed-decisions, including its JSON-encoded state, questions, and gold columns:

python3 scripts/laya/prepare_laya_finetune.py typed-decisions-train.jsonl --output train.jsonl
python3 scripts/laya/prepare_laya_finetune.py typed-decisions-eval.jsonl --output eval.jsonl

Keep every question from a source case in the same split. The trainer rejects cross-split ID, group, source-text, and token-sequence overlap. Unlike the notebook, calibration uses a separate optional calibration_file; it never fits on the training or evaluation examples.

Example job.json (all paths must be absolute):

{
  "version": 1,
  "model_dir": "/models/extractors/laya",
  "train_file": "/data/laya/train.jsonl",
  "eval_file": "/data/laya/eval.jsonl",
  "output_dir": "/runs/laya-domain-v1",
  "backend": "metal",
  "objective": "rlcd",
  "epochs": 4,
  "batch_size": 1,
  "gradient_accumulation": 4,
  "encoder_lr": 0.000025,
  "head_lr": 0.0001,
  "head_dropout": 0.1,
  "seed": 42
}

The output directory must be new. The run writes metrics.jsonl, resumable latest.safetensors, and a final report.json containing data/run hashes, initial/final evaluation metrics, and optimizer progress. A completed run publishes model/, which can be loaded as an ordinary Laya extractor. The report's complete status means training and export completed; assess its held-out results before deploying the checkpoint.

To resume, keep the original job settings, set resume_from to the previous latest.safetensors, and choose a new output directory. Checkpoints bind the source weights, tokenizer, configuration, datasets, and training settings. They preserve AdamW moments and partial gradient accumulation. The optional stop_after_microbatches setting saves a resumable checkpoint at a safe boundary without exporting a serving model. Checkpoints are otherwise saved every checkpoint_every_steps microbatches (default 100) and at epoch boundaries.

The action head remains frozen because these targets do not supervise it. Changing the encoder can still change action probabilities, so validate those separately. Old calibration buckets are removed on export. Without a calibration split, temperatures reset to one. With one, a bounded per-type temperature search fits only types having at least ten calibration examples.

Training uses a bounded host allocator (max_host_bytes, default 24 GiB); start with small batches. CUDA/DDP and mixed-precision training are not implemented.

Reference validation, resident-path measurements, and training parity results are in Laya validation and measurements.