Laya β€” ONNX (fp16)

Laya exported to ONNX, so it runs under ONNX Runtime β€” on a CPU, a GPU, or in a browser β€” with no PyTorch installed. This is the default build.

Laya is not a generative model. Give it a state (text, email, ticket, JSON) and typed questions, and it returns one calibrated distribution per question in a single forward pass. Nothing is sampled, so the answer is always one of the options you supplied.

pip install onnxruntime huggingface_hub tokenizers numpy
hf download inferenceprince/laya-onnx --local-dir laya-onnx
File Size What
model.onnx 3.3 MB the computation graph
model.onnx.data 842.6 MB fp16 weights
tokenizer/ 3.6 MB unchanged from the base model
rl_agent_config.json β€” max_len, head_max_len, fitted temperatures

The weights are the same size as the base checkpoint's model.safetensors (842.6 MB) because they are the same fp16 tensors. The 3.3 MB graph file is the only addition β€” safetensors carries no architecture, so the structure lives in the authors' Python; ONNX has to serialise it. That is what buys you a runtime with no framework dependency.

How to use it

The graph takes five inputs and returns logits and act_logits. Everything else β€” the prompt format, the tokeniser, the temperature β€” is in the two files beside it. This is the whole thing:

import json
import numpy as np
import onnxruntime as ort
from tokenizers import Tokenizer

MODEL_DIR = "laya-onnx"

config = json.load(open(MODEL_DIR + "/rl_agent_config.json"))
tokenizer = Tokenizer.from_file(MODEL_DIR + "/tokenizer/tokenizer.json")
session = ort.InferenceSession(MODEL_DIR + "/model.onnx", providers=["CPUExecutionProvider"])

CLS = tokenizer.token_to_id("[CLS]")
SEP = tokenizer.token_to_id("[SEP]")
MASK = tokenizer.token_to_id("[MASK]")


def tokenize(text):
    """Text to a list of token ids."""
    return tokenizer.encode(text, add_special_tokens=False).ids


def temperature_for(question_type, option_count):
    """The fitted temperature for this kind of question."""
    if option_count <= 2:
        size = "2"
    elif option_count <= 5:
        size = "3-5"
    elif option_count <= 10:
        size = "6-10"
    else:
        size = "11+"

    key = question_type + ":" + size
    if key in config["temperature_by_options"]:
        return config["temperature_by_options"][key]
    return config["temperature"][0]


# The question we want answered.
state = "I was billed twice. Please refund the duplicate."
question = "Which team should handle this?"
options = {
    "billing": "invoices, payments, refunds",
    "technical": "bugs, outages",
    "sales": "pricing",
}

# Build the prompt. The model reads:
#   [CLS] choice question: <question> [SEP]
#   [MASK] billing: ...  [MASK] technical: ...  [MASK] sales: ... [SEP]
#   <the text to analyse> [SEP]
# Every option gets its own [MASK] token, and the model scores that position.
token_ids = [CLS] + tokenize("choice question: " + question) + [SEP]

option_positions = []          # where each option's [MASK] sits in token_ids
for label, description in options.items():
    option_positions.append(len(token_ids))
    token_ids.append(MASK)
    token_ids += tokenize(" " + label + ": " + description)[:48]

token_ids.append(SEP)

# Whatever room is left in the 512-token window goes to the text being analysed.
room = config["max_len"] - len(token_ids) - 1
token_ids += tokenize(state)[:room]
token_ids.append(SEP)

# Run the model. It returns one raw score per option.
option_count = len(option_positions)
outputs = session.run(None, {
    "input_ids":      np.array([token_ids], dtype=np.int64),
    "attention_mask": np.ones((1, len(token_ids)), dtype=np.int64),
    "marker_pos":     np.array([option_positions], dtype=np.int64),
    "marker_mask":    np.ones((1, option_count), dtype=bool),
    "qtype":          np.array([0], dtype=np.int64),   # 0=choice, 1=score, 2=noul
})
logits = outputs[0]

# Turn scores into probabilities. The graph returns raw scores; dividing by the
# fitted temperature is what makes them meaningful, so this step is not optional.
temperature = temperature_for("choice", option_count)
scores = logits[0, :option_count] / temperature

scores = scores - scores.max()          # keep the numbers small before exponentiating
probabilities = np.exp(scores)
probabilities = probabilities / probabilities.sum()

# Print the winner and the full distribution.
labels = list(options.keys())
print("answer:", labels[int(probabilities.argmax())])
for label, probability in zip(labels, probabilities):
    print("  %-12s %.4f" % (label, probability))

Real output from running the above against this build:

answer: billing
  billing      0.9696
  technical    0.0152
  sales        0.0152

Notes that matter:

  • Options must fit head_max_len (192 tokens). The instructions share that budget with the options, and options are individually capped at 48 tokens. Long labels get silently truncated β€” keep choice under ~20 options.
  • marker_pos must be an int64 array of length β‰₯ the number of options, and marker_mask must be True for the real ones. Pad both if you batch.
  • Divide the logits by the temperature before softmax. The graph returns raw logits; the fitted values in rl_agent_config.json are what make the probabilities meaningful.
  • Batching: rows are independent, so several questions can share one forward pass. Each row needs its own marker_pos, marker_mask and qtype.

Startup

Measured on an Intel i5-14400F (6 performance + 4 efficiency cores), CPU only, from a cold process:

PyTorch this build
Load to first answer 25–35 s 3–5 s

Startup is the largest difference between the two, and it is stable across runs. Inference latency is broadly comparable β€” in the quietest measurements roughly 150 ms for one short question against 200 ms for PyTorch β€” but this machine is shared and load swung identical work by more than 10x between runs, so treat latency as indicative rather than as a specification.

Accuracy

116 synthetic items across 15 categories. The labels were generated by a language model, not written by human annotators. So these figures measure agreement with a model-authored rubric rather than independent accuracy, and any shared bias between the labels and the model under test would flatter the result. Treat them as a smoke test that a build behaves like the reference, and validate on your own data before relying on the numbers.

Type n Accuracy
noul 51 86.3%
choice 45 77.8%
score 20 50.0%
overall 116 76.7%

Fidelity: the same answer as the upstream PyTorch model on all 116 items. Worst probability shift 1.8e-03. Tokenisation and prompt construction are byte-identical to the reference.

Where it works: classification with genuinely distinct categories β€” routing, prompt-injection detection, refund/churn/phishing detection, agent-trace failure labelling. 87–100% on those.

Where it does not: fine procedural distinctions (invoice hold vs request_info vs reject: 25%) and abstract severity ladders (16.7%). Ordinal score is coarse β€” never wrong by more than one level, but only half the time exactly right. choice confidence reads far lower than its real accuracy, so recalibrate on your own data before automating on it.

Limits

  • English only. Collapses on non-Latin scripts while staying confident, so confidence gating will not catch it. Use laya-multilingual for anything else.
  • Not a zero-shot decision engine. The authors state the base checkpoints are near chance on their own typed-decisions benchmark; accuracy comes from fine-tuning on your data.
  • Keep choice under ~20 options β€” options share a fixed 192-token prompt budget.
  • act_probability carries no signal β€” it saturates near 1.0 on every input tested.

Attribution

Model, training and weights by Nandakishor M and Convai Innovations, Apache-2.0. This is an independent ONNX conversion, not an official Convai Innovations release.

Downloads last month
24
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Model tree for inferenceprince/laya-onnx

Quantized
(16)
this model