A .NET port of Laya: typed decisions (choice/score/noul)
over state, answered in one forward pass by a small transformer decision head — no text generation,
so there is nothing to parse and nothing to hallucinate.
Ships two interchangeable inference backends behind one interface, so they can be benchmarked against each other on the same requests:
- Layar.Onnx — ONNX Runtime
- Layar.TorchSharp — TorchSharp (libtorch), loading a
torch.jit.trace-exported TorchScript module rather than a hand-ported architecture
Both are verified bit-for-bit (well, float-for-float) against the original Python package's own
output on the same request — see tools/layar-export and the numbers below.
| Package | What it is |
|---|---|
Layar |
Question/answer types, sequence building, calibration math, language/script routing, typed answer accessors. No ML dependency; AOT-safe. |
Layar.Tokenization |
A from-scratch BPE tokenizer (byte-level GPT-2-style + Metaspace/SentencePiece-style) reading a Hugging Face tokenizer.json directly. No external tokenizer package, no Python at runtime. |
Layar.Onnx |
IDecisionBackend over ONNX Runtime. |
Layar.TorchSharp |
IDecisionBackend over TorchSharp/libtorch. |
Layar.Cli (layar) |
predict and benchmark commands, installable as a dotnet tool. |
Layar.Onnx.Cpu |
Meta-package: Layar + Layar.Tokenization + Layar.Onnx in one install — the default, since ONNX Runtime doesn't need picking a native runtime variant the way TorchSharp does. |
Layar.TorchSharp.Cpu |
Meta-package: Layar + Layar.Tokenization + Layar.TorchSharp + the TorchSharp-cpu native runtime in one install. |
Install the meta-package that matches how you want to run it, or the individual Layar.* packages
for full control (e.g. both backends side by side, to compare them):
dotnet add package Layar.Onnx.Cpu # ONNX Runtime, one package
# or
dotnet add package Layar.TorchSharp.Cpu # TorchSharp/libtorch, one package
# or, for full control / both backends:
dotnet add package Layar
dotnet add package Layar.Tokenization
dotnet add package Layar.Onnx
dotnet add package Layar.TorchSharp
dotnet add package TorchSharp-cpuvar tokenizer = HuggingFaceBpeTokenizer.FromDirectory("path/to/checkpoint/tokenizer");
using var backend = new OnnxDecisionBackend("path/to/checkpoint/model.onnx"); // or TorchSharpDecisionBackend(...model.pt)
using var engine = new DecisionEngine(tokenizer, backend, maxLen: 1024, headMaxLen: 256);
// TriageSchema.Instance is IQuestionSchema<TriageResult>: a fixed question set plus how to bind
// the answers back into a record, so this returns TriageResult directly — no string-keyed lookup.
var triage = await engine.PredictAsync(
"Hi, we were billed twice for March. Please refund the duplicate today.",
TriageSchema.Instance);
Console.WriteLine($"{triage.Intent.Choice} (confidence {triage.Intent.Confidence:F2})");
// A caller-composed question set (e.g. EmailQuestions() with your own categories) has no fixed
// shape to bind to, so it stays on the dictionary path — same call, typed accessors instead:
var answers = await engine.PredictAsync("...", Presets.EmailQuestions(myCategories));
var category = answers.Choice("category");A checkpoint directory (model.onnx, model.pt, tokenizer/, config.json) is produced by
tools/layar-export/export.py, a maintainer-run Python script that loads the original laya
package's checkpoint and exports it — not shipped with the .NET library.
Checkpoints (650MB-1.2GB) aren't committed to this repo, and they don't release on the same tags as
the library — a models-<version> tag (e.g. models-1.0.0), independent of the library's own
v<version> tags, triggers .github/workflows/release-models.yml to export and upload them; see
"Releases" below. CheckpointCache downloads and caches one on first use — where they're hosted is
pluggable (CheckpointAssetUrl); GitHubReleaseCheckpointUrls covers the case of attaching
tools/layar-export's output to a GitHub Release as flat assets:
using var cache = new CheckpointCache(
cacheRoot: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "layar", "models"),
assetUrl: GitHubReleaseCheckpointUrls.Create("nullean", "layar", "models-1.0.0"));
var tokenizerDir = await cache.EnsureTokenizerAsync("multilingual");
var onnxPath = await cache.EnsureOnnxModelAsync("multilingual"); // also fetches model.onnx.data if presentNo checkpoints have actually been published there yet — that needs the repo pushed to GitHub and
models-1.0.0 tagged, not a code change, so it hasn't been done as part of this port. The
checkpoint itself is Apache-2.0 and explicitly tagged for commercial use — see
NOTICE.md for the license check behind redistributing it.
Two independent tag prefixes, so a library release never re-uploads unchanged model weights and a new checkpoint export never forces a library version bump (MinVer's own pattern for this — see its README, "Can I version multiple projects in a single repository independently?"):
| Tag | Triggers | Versions |
|---|---|---|
v<major.minor.patch>, e.g. v0.2.0 |
.github/workflows/ci.yml |
Layar, Layar.Tokenization, Layar.Onnx, Layar.TorchSharp, Layar.Cli and the meta-packages (MinVerTagPrefix is v) |
models-<major.minor.patch>, e.g. models-1.0.0 |
.github/workflows/release-models.yml |
The three checkpoints (english, multilingual, typed-decisions), exported fresh and uploaded as flat GitHub Release assets |
tests/Laya.Benchmarks (BenchmarkDotNet, dotnet run -c Release --project tests/Laya.Benchmarks -- --filter '*')
on this repo's multilingual checkpoint, a real 3-question batched request, CPU, Apple M2:
| Method | Mean | Ratio | Allocated |
|---|---|---|---|
| Onnx | 148.7 ms | 1.00 | 82.6 KB |
| TorchSharp | 70.9 ms | 0.48 | 85.3 KB |
TorchSharp is ~2.1x faster here. Both match the Python oracle exactly (confidence agreement to
1e-6) — see tests/Laya.Tests/PipelineParityTests.cs. SessionOptions tuning (thread count,
graph optimization level, execution mode) doesn't close the gap — see AGENTS.md's rough edges
for what was tried.
dotnet build layar.slnx
dotnet run --project tests/Laya.Tests/Laya.Tests.csproj -c ReleaseTorchSharp's native libtorch needs DYLD_LIBRARY_PATH/LD_LIBRARY_PATH pointed at the
consolidated native library directory when running via dotnet run from a loose build output (see
AGENTS.md). Separately, on macOS, TorchSharp-cpu's libtorch_cpu.dylib hardcodes an absolute
load path to homebrew's libomp.dylib — brew install libomp fixes it, or point
DYLD_LIBRARY_PATH at wherever libomp.dylib already landed (a published/AOT output bundles its
own copy right next to the binary — see AGENTS.md's rough edges for why dyld still needs pointing
at it).
This is a working, numerically-verified port of the full core surface: Core, Tokenization, Onnx,
TorchSharp, a minimal Cli, the model-lifecycle Router (RouterEngine: load, preload, unload,
attach, LRU eviction — the pure routing decision was ported first; the loading side wraps it),
structured/non-string criteria (CriterionText, matching Python's render_criterion), on-demand
checkpoint downloading (CheckpointCache + GitHubReleaseCheckpointUrls, though nothing's
published there yet — see above), and the embedding shortlist for >20-option questions
(Shortlist + ShortlistPrediction, matching shortlist.py/predict_shortlist exactly, including
its tie-breaking and NaN/zero-vector edge cases). Tokenizer conformance and full-pipeline parity
against the Python oracle are committed regression tests, not one-off scratchpad checks — see
tests/Laya.Tests. tests/Laya.Benchmarks has the real ONNX-vs-TorchSharp numbers (BenchmarkDotNet,
not ad-hoc timing).
One deliberate gap: Python's embed_fn_from_agent (embeddings derived from the loaded checkpoint's
own encoder) has no equivalent — IDecisionBackend only exposes the decision head's final output,
not raw encoder hidden states, so that would need a third exported graph tools/layar-export
doesn't produce today. Shortlist itself works with any IEmbeddingProvider, including one backed
by a real bi-encoder (which Python's own docs note usually shortlists better anyway).
Not yet done: the full docs site. ONNX Runtime's ~2x latency gap vs. TorchSharp was investigated
(SessionOptions thread/execution-mode tuning, see "Measured" above) and isn't a quick fix — see
AGENTS.md's rough edges. Filed
dotnet/TorchSharp#1581 asking about Metal/MPS
backend availability.
NativeAOT compatibility is now verified, both examples/layar-aot-smoketest (Onnx) and
examples/layar-torchsharp-aot-smoketest (TorchSharp), and — beyond what those check, since a real
checkpoint can't ship in CI — a NativeAOT binary was confirmed locally to load a real exported
checkpoint through Laya.TorchSharp and produce a correct prediction. Layar (root), Laya.Onnx
and Laya.TorchSharp all declare IsAotCompatible. This machine's Xcode Command Line Tools SDK is
still independently broken (see AGENTS.md); verifying past that needed an explicit SDKROOT
override, not a fix to the SDK itself.