laya 0.3.2__tar.gz → 0.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: laya
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Fast, non-autoregressive System 1 decision engine with calibrated probabilities
5
5
  Author: Convai Innovations
6
6
  License: Apache-2.0
@@ -28,10 +28,17 @@ Requires-Dist: numpy>=1.20.0
28
28
  Dynamic: license-file
29
29
  Dynamic: requires-python
30
30
 
31
- # Laya
31
+ <p align="center">
32
+ <picture>
33
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/logo-lockup-dark.png" />
34
+ <img src="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/logo-lockup.png" alt="Laya" width="330" />
35
+ </picture>
36
+ </p>
32
37
 
33
38
  **Multilingual, non-autoregressive System 1 decision engine.** Typed decisions over 100+ languages in a single forward pass — 33 ms — trained with reinforcement learning against strictly proper scoring rules (RLCD), with a router that picks the right checkpoint per request.
34
39
 
40
+ <div align="center">
41
+
35
42
  [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/15d4Yv__KHeHjshVb-6PRTfqVllxih2S3?usp=sharing)
36
43
  [![PyPI version](https://img.shields.io/pypi/v/laya.svg)](https://pypi.org/project/laya/)
37
44
  [![Hugging Face Model](https://img.shields.io/badge/%F0%9F%A4%97%20Model-convaiinnovations%2Flaya-blue)](https://huggingface.co/convaiinnovations/laya)
@@ -41,8 +48,10 @@ Dynamic: requires-python
41
48
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-nandakishorm-FFDD00?logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/nandakishorm)
42
49
  [![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](https://opensource.org/licenses/Apache-2.0)
43
50
 
51
+ </div>
52
+
44
53
  <p align="center">
45
- <img src="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/laya_vs_jev.png" alt="Laya with routing versus TypeSafe Jev: accuracy on shared public datasets, language coverage, speed and calibration" width="100%" />
54
+ <img src="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/laya_vs_jev_full.png" alt="Laya versus TypeSafe Jev: accuracy on shared public datasets, every application workflow, all 51 languages, speed, calibration, and the cost of not preloading" width="100%" />
46
55
  </p>
47
56
 
48
57
  Laya evaluates typed questions (`choice`, `score`, `noul`) over any state (text, email, ticket or JSON document) in **a single forward pass** — 33 ms for one question, 7.2 ms/question batched, measured on a T4. No text generation, so nothing to parse and nothing to hallucinate.
@@ -65,27 +74,29 @@ pip install laya
65
74
 
66
75
  ---
67
76
 
68
- ## Quickstart
77
+ ## Quickstart: Route Mode (Recommended)
78
+
79
+ Laya ships three checkpoints. The built-in **`Router`** is the recommended entry point: it evaluates any state in any language, automatically detects scripts and languages in sub-milliseconds, and dispatches to the optimal checkpoint in a single forward pass.
69
80
 
70
81
  ```python
71
82
  import laya
83
+ from laya import Router
72
84
 
73
- # 1. Load the fine-tuned model directly from Hugging Face Hub (auto-downloads weights)
74
- agent = laya.load("convaiinnovations/laya")
85
+ # Preload checkpoints into memory for instant sub-35ms routing
86
+ router = Router(preload=True)
75
87
 
76
- # 2. Provide any state (string or dictionary)
88
+ # 1. State in any language or schema
77
89
  state = {
78
90
  "from": "user@acme.com",
79
91
  "subject": "Duplicate charge on invoice #4411",
80
92
  "body": "Hi, we were billed twice for March. Please refund the duplicate today or we will cancel our plan."
81
93
  }
82
94
 
83
- # 3. Define your typed questions
95
+ # 2. Define your typed questions
84
96
  questions = {
85
- # choice: categorical selection with probabilities & confidence
86
97
  "department": {
87
98
  "type": "choice",
88
- "instructions": "Which department should handle this email?",
99
+ "instructions": "Which department should handle this request?",
89
100
  "criteria": {
90
101
  "billing": "invoices, payments, refunds",
91
102
  "technical": "bugs, outages, system errors",
@@ -93,38 +104,117 @@ questions = {
93
104
  "other": "everything else"
94
105
  }
95
106
  },
96
- # score: placement on an ordinal rubric
97
107
  "urgency": {
98
108
  "type": "score",
99
109
  "instructions": "How urgent is this request?",
100
110
  "criteria": ["not urgent", "soon", "critical deadline or blocking issue"]
101
111
  },
102
- # noul: calibrated boolean probability P(true)
103
112
  "churn_risk": {
104
113
  "type": "noul",
105
114
  "instructions": "Does the user threaten to cancel or leave?"
106
115
  },
107
- "is_phishing": {
116
+ "refund_requested": {
108
117
  "type": "noul",
109
- "instructions": "Is this email a phishing or scam attempt?"
118
+ "instructions": "Does the user explicitly request a refund?"
110
119
  }
111
120
  }
112
121
 
113
- # 4. Run all questions in ONE single forward pass (~35 ms on GPU)
114
- result = agent.predict(state, questions)
115
- answers = result["answers"]
122
+ # 3. English state -> automatically routed to laya (ModernBERT-large, 39.5 ms)
123
+ res_en = router.predict(state, questions)
124
+ print("Department :", res_en["answers"]["department"]["choice"]) # -> billing (confidence: 0.94)
125
+ print("Routing :", res_en["routing"]["model"]) # -> english
126
+
127
+ # 4. Hindi state -> automatically routed to laya-multilingual (mmBERT-base, 32.8 ms)
128
+ res_hi = router.predict({"body": "मुझसे दो बार शुल्क लिया गया, कृपया पैसे वापस करें।"}, questions)
129
+ print("Department :", res_hi["answers"]["department"]["choice"]) # -> billing (confidence: 0.86)
130
+ print("Routing :", res_hi["routing"]["model"]) # -> multilingual
131
+
132
+ # 5. Explicit override when you want a specific checkpoint
133
+ res_td = router.predict(state, questions, model="typed-decisions")
134
+ ```
135
+
136
+ Every result carries full routing metadata explaining why the choice was made:
137
+
138
+ ```python
139
+ res_hi["routing"]
140
+ # {
141
+ # 'model': 'multilingual',
142
+ # 'repo': 'convaiinnovations/laya/multilingual',
143
+ # 'reason': 'non-Latin script (devanagari, 100% of letters); the English checkpoint cannot read it'
144
+ # }
145
+ ```
146
+
147
+ Inspect a routing decision without running any forward pass:
148
+
149
+ ```python
150
+ router.route({"body": "Der Kunde wurde zweimal belastet"}, questions).reason
151
+ # "Latin script but language looks like 'de', not English"
152
+ ```
116
153
 
117
- print("Department :", answers["department"]["choice"])
118
- # -> billing (confidence: 0.94)
154
+ ### Why Route: The Evidence
119
155
 
120
- print("Urgency :", answers["urgency"]["score"])
121
- # -> 1.84 / 2.0
156
+ On a shared benchmark (17,416 questions, one T4 GPU, identical questions per model):
122
157
 
123
- print("Churn Risk :", answers["churn_risk"]["noul"])
124
- # -> 0.892 (89.2% probability)
158
+ | Benchmark / Task | English (`laya`) | Multilingual (`laya-multilingual`) | `Router` (Routed) |
159
+ |---|---|---|---|
160
+ | MASSIVE intent, English | **0.783** | 0.657 | **0.783** |
161
+ | MASSIVE intent, 13 other languages | 0.306 | **0.451** | **0.451** |
162
+ | XNLI, English | **0.860** | 0.843 | **0.860** |
163
+ | XNLI, 14 other languages | 0.521 | **0.731** | **0.731** |
164
+ | Languages usable (>3x random) | 23 / 51 | 45 / 51 | **45 / 51** |
165
+ | Latency, 1 question (T4 GPU) | 39.5 ms | **32.8 ms** | **32.8 ms** |
166
+ | Latency, 10 questions batched | 158.6 ms | **72.3 ms** | **72.3 ms** |
167
+
168
+ The English checkpoint collapses on non-Latin scripts (Khmer scores **0.000 accuracy at 0.952 confidence**). Because the model stays confident while being wrong, confidence gating cannot save you. `Router` detects the script in <0.5 ms pure Python before the forward pass.
169
+
170
+ ### Production Preload & Memory
125
171
 
126
- print("Phishing :", answers["is_phishing"]["noul"])
127
- # -> 0.008 (0.8% probability)
172
+ A cold checkpoint build costs seconds; language detection costs microseconds. At the default `max_loaded=1`, traffic that alternates languages rebuilds a model on *every* request (measured at a 7.4 s median reload on CPU and 10.3 s on T4).
173
+
174
+ For a server or production app, preload:
175
+
176
+ ```python
177
+ # Every checkpoint resident in memory; language flips cost detection only (<1 ms)
178
+ router = Router(preload=True)
179
+ router = Router(preload=True, device="cuda")
180
+
181
+ # Or preload only the specific checkpoints you serve:
182
+ router.preload(["english", "multilingual"])
183
+
184
+ # If your app already built an agent, attach it to avoid duplicate VRAM:
185
+ router.attach("english", existing_agent)
186
+
187
+ # Manage resident memory (default keeps 1 hot, LRU eviction)
188
+ router = Router(max_loaded=2) # keep two hot
189
+ router.unload() # free memory
190
+ ```
191
+
192
+ | Deployment Mode | Per-Request Latency | Model Reloads |
193
+ |---|---|---|
194
+ | `Router()` (lazy, `max_loaded=1`) | 7 to 10 s on every language switch | 1 per switch |
195
+ | `Router(preload=True)` | **32.8 ms (GPU) / 193–464 ms (CPU)** | **none** |
196
+
197
+ ---
198
+
199
+ ## Single-Model Mode (Direct SDK)
200
+
201
+ If you only need a single checkpoint for a dedicated pipeline, you can load models directly:
202
+
203
+ ```python
204
+ import laya
205
+
206
+ # 1. Load a specific checkpoint directly from the hub
207
+ agent = laya.load("convaiinnovations/laya") # English root
208
+ agent_ml = laya.load("convaiinnovations/laya", subfolder="multilingual") # 100+ languages
209
+ agent_td = laya.load("convaiinnovations/laya", subfolder="typed-decisions")
210
+
211
+ # 2. Run all questions in ONE single forward pass (~35 ms on GPU)
212
+ result = agent.predict(state, questions)
213
+ answers = result["answers"]
214
+
215
+ print("Department :", answers["department"]["choice"]) # -> billing (confidence: 0.94)
216
+ print("Urgency :", answers["urgency"]["score"]) # -> 1.84 / 2.0
217
+ print("Churn Risk :", answers["churn_risk"]["noul"]) # -> 0.892 (89.2% probability)
128
218
  ```
129
219
 
130
220
  ---
@@ -171,94 +261,6 @@ triage = agent.predict({"message": "My payment failed twice"}, laya.triage_quest
171
261
 
172
262
  ---
173
263
 
174
- ## Model Routing (three checkpoints, one call)
175
-
176
- Laya ships three checkpoints. `Router` picks the right one per request and loads it lazily.
177
-
178
- | name | repo | size | context | best at |
179
- |---|---|---|---|---|
180
- | `english` | [`convaiinnovations/laya`](https://huggingface.co/convaiinnovations/laya) | 421M | 512 | English text |
181
- | `multilingual` | [`convaiinnovations/laya-multilingual`](https://huggingface.co/convaiinnovations/laya-multilingual) | 322M | 1024 | 100+ languages, 2x faster |
182
- | `typed-decisions` | [`convaiinnovations/laya-typed-decisions`](https://huggingface.co/convaiinnovations/laya-typed-decisions) | 421M | 1024 | the four typed-decisions workflows |
183
-
184
- ```python
185
- from laya import Router
186
-
187
- router = Router() # nothing is downloaded until a request needs it
188
-
189
- # English -> routed to the English checkpoint
190
- router.predict({"body": "I was charged twice, please refund."}, questions)
191
-
192
- # Hindi -> routed to the multilingual checkpoint automatically
193
- router.predict({"body": "मुझसे दो बार शुल्क लिया गया"}, questions)
194
-
195
- # explicit when you already know
196
- router.predict(state, questions, model="typed-decisions")
197
- router.predict(state, questions, lang="de")
198
- ```
199
-
200
- Every result carries the decision that produced it:
201
-
202
- ```python
203
- result = router.predict({"body": "二重に請求されました"}, questions)
204
- result["routing"]
205
- # {'model': 'multilingual',
206
- # 'repo': 'convaiinnovations/laya-multilingual',
207
- # 'reason': 'non-Latin script (kana, 100% of letters); the English checkpoint cannot read it',
208
- # ...}
209
- ```
210
-
211
- Inspect a decision without running the model:
212
-
213
- ```python
214
- router.route({"body": "Der Kunde wurde zweimal belastet"}, questions).reason
215
- # "Latin script but language looks like 'de', not English"
216
- ```
217
-
218
- ### Why route at all
219
-
220
- Accuracy on a shared benchmark (17,416 questions, one T4, identical questions per model):
221
-
222
- | | `english` | `multilingual` |
223
- |---|---|---|
224
- | MASSIVE intent, English | **0.783** | 0.657 |
225
- | MASSIVE intent, 13 other languages | 0.306 | **0.451** |
226
- | XNLI, English | **0.860** | 0.843 |
227
- | XNLI, 14 other languages | 0.521 | **0.731** |
228
- | English-only suites | **0.684** | 0.619 |
229
- | Latency, 10 questions | 159 ms | **72 ms** |
230
-
231
- The English checkpoint does not degrade gracefully outside English -- it collapses, and stays
232
- confident while doing so. On 20-option MASSIVE intent (random = 0.050) it scores 0.100 on Hindi
233
- and 0.103 on Korean, with an expected calibration error of 0.855. Script detection is therefore
234
- the primary routing signal.
235
-
236
- ### Routing rules
237
-
238
- Precedence, highest first:
239
-
240
- 1. `model=` -- explicit checkpoint.
241
- 2. `task="typed_decisions"` -- explicit task.
242
- 3. A question-id set exactly matching a typed-decisions workflow, **only** if you construct the
243
- router with `auto_task_detection=True`. It is off by default: that checkpoint is fine-tuned on
244
- four synthetic workflows and should not be a silent fallback.
245
- 4. `lang=` -- explicit language code.
246
- 5. Detected script (exact) and, for Latin text, a stopword/diacritic language guess (best effort).
247
- 6. `default=` (`"english"` unless you change it).
248
-
249
- ### Memory
250
-
251
- All three together are ~1.16B parameters, so `Router` keeps **one** resident by default and
252
- evicts least-recently-used:
253
-
254
- ```python
255
- Router(max_loaded=2) # keep two hot
256
- router.unload() # free everything
257
- router.loaded # ['multilingual']
258
- ```
259
-
260
- ---
261
-
262
264
  ## Decision Primitives
263
265
 
264
266
  | Primitive | Output | Use Cases |
@@ -307,7 +309,8 @@ published, never measured here** (no TypeSafe API access), so sample sizes and p
307
309
  | typed-decisions, 2,000 decisions | 0.727 | **0.766** | +0.039 |
308
310
  | AG News, 4 labels | 0.910 | **0.950** | +0.040 |
309
311
  | DAIR Emotion, 6 labels | 0.480 | **0.595** | +0.115 |
310
- | ECE *(lower better)* | 0.246 | **0.081** | 3× better |
312
+ | Banking77 (72 vs 77 labels) | **0.870** | 0.425 | Jev leads on >20 options |
313
+ | ECE *(lower better)* | 0.246 | **0.081** | 3× better (post-temperature) |
311
314
  | p50 latency, 1 question | 236–276 ms | **32.8 ms** | 7.8× faster |
312
315
  | Languages usable | *no published benchmark* | **45 of 51** | — |
313
316
  | Weights | closed API | **Apache 2.0** | — |
@@ -316,6 +319,12 @@ published, never measured here** (no TypeSafe API access), so sample sizes and p
316
319
  On DAIR Emotion, Jev assigned **zero probability to the true label on 16% of examples** — a hard
317
320
  failure for anything branching on confidence.
318
321
 
322
+ #### Where Jev leads
323
+
324
+ * **High-cardinality label spaces (>20 options at default settings):** On Banking77, Jev scores 0.870 (on 72 labels) while Laya scores 0.425 (on 77 labels at default 256-token head budget). This is an architectural token-budget constraint: options share a fixed `head_max_len` budget (192 tokens on English, 256 on multilingual), so 77 options receive only ~3 to 4 tokens per label, causing text to become indistinguishable. Jev supports up to 255 options out-of-the-box. While `laya-multilingual` supports 1,024 context (and up to 8,192 in the encoder) and you can raise `agent.cfg["head_max_len"] = 512` at runtime, Jev is currently better suited for 50+ options in a single prompt without tuning.
325
+ * **Soft distribution matching:** On typed-decisions, while Laya achieves higher argmax accuracy (0.766 vs 0.727), Jev achieves higher soft accuracy (0.580 vs 0.471) against the teacher's full probability distributions.
326
+ * **Out-of-the-box raw calibration:** Before temperature scaling, the base checkpoint has higher raw ECE (0.213 vs 0.144). Laya achieves its 0.081 ECE after domain temperature fitting.
327
+
319
328
  Full detail, including every workflow and all 51 languages: **[`BENCHMARKS.md`](BENCHMARKS.md)**.
320
329
 
321
330
  ### typed-decisions, measured on all three checkpoints
@@ -382,11 +391,12 @@ temperatures at all, so fit them before relying on its probabilities.
382
391
  against a 0.318 random baseline and a 0.461 majority-class baseline. The 0.766 figure comes
383
392
  from the checkpoint fine-tuned on that benchmark's own training split. Laya is a fast base to
384
393
  specialise, not a zero-shot decision engine.
385
- * **Keep `choice` questions under ~20 options.** Every option is rendered into a fixed
386
- `head_max_len` budget (192 tokens on `laya`, 256 on the others), so a 77-option question
387
- leaves roughly 4 tokens per label and the option text stops being distinguishable —
388
- accuracy falls off sharply. Split large label spaces into a coarse choice followed by a
389
- fine one.
394
+ * **High-cardinality choice questions and token budgets:** Sequences split into an option prompt budget (`head_max_len`) and the remaining document/state budget (`max_len - head_max_len`):
395
+ * `laya` (English) defaults to 512 context (`head_max_len = 192`, ~320 tokens for state).
396
+ * `laya-multilingual` and `laya-typed-decisions` default to 1,024 context (`head_max_len = 256`, ~768 tokens for state; mmBERT-base encoder supports up to 8,192 with RoPE).
397
+ At default settings, a 77-option question like Banking77 allocates only `(256 - 16) // 77` ≈ 3–4 tokens per label, which causes accuracy to fall off sharply (0.425 vs Jev's 0.870). If evaluating 50+ options in a single question:
398
+ 1. Raise `agent.cfg["head_max_len"] = 512` and `agent.cfg["max_len"] = 1024` (or up to 2048 / 4096 / 8192) so every option has enough tokens to remain distinct.
399
+ 2. Or split large option sets into a two-step coarse-to-fine hierarchical choice.
390
400
  * Ordinal `score` questions are the weakest primitive (SST-5 0.372).
391
401
  * `laya` collapses outside English; `laya-multilingual` is weaker on English. Route, or pick
392
402
  deliberately.
@@ -1,7 +1,14 @@
1
- # Laya
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/logo-lockup-dark.png" />
4
+ <img src="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/logo-lockup.png" alt="Laya" width="330" />
5
+ </picture>
6
+ </p>
2
7
 
3
8
  **Multilingual, non-autoregressive System 1 decision engine.** Typed decisions over 100+ languages in a single forward pass — 33 ms — trained with reinforcement learning against strictly proper scoring rules (RLCD), with a router that picks the right checkpoint per request.
4
9
 
10
+ <div align="center">
11
+
5
12
  [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/15d4Yv__KHeHjshVb-6PRTfqVllxih2S3?usp=sharing)
6
13
  [![PyPI version](https://img.shields.io/pypi/v/laya.svg)](https://pypi.org/project/laya/)
7
14
  [![Hugging Face Model](https://img.shields.io/badge/%F0%9F%A4%97%20Model-convaiinnovations%2Flaya-blue)](https://huggingface.co/convaiinnovations/laya)
@@ -11,8 +18,10 @@
11
18
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-nandakishorm-FFDD00?logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/nandakishorm)
12
19
  [![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](https://opensource.org/licenses/Apache-2.0)
13
20
 
21
+ </div>
22
+
14
23
  <p align="center">
15
- <img src="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/laya_vs_jev.png" alt="Laya with routing versus TypeSafe Jev: accuracy on shared public datasets, language coverage, speed and calibration" width="100%" />
24
+ <img src="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/laya_vs_jev_full.png" alt="Laya versus TypeSafe Jev: accuracy on shared public datasets, every application workflow, all 51 languages, speed, calibration, and the cost of not preloading" width="100%" />
16
25
  </p>
17
26
 
18
27
  Laya evaluates typed questions (`choice`, `score`, `noul`) over any state (text, email, ticket or JSON document) in **a single forward pass** — 33 ms for one question, 7.2 ms/question batched, measured on a T4. No text generation, so nothing to parse and nothing to hallucinate.
@@ -35,27 +44,29 @@ pip install laya
35
44
 
36
45
  ---
37
46
 
38
- ## Quickstart
47
+ ## Quickstart: Route Mode (Recommended)
48
+
49
+ Laya ships three checkpoints. The built-in **`Router`** is the recommended entry point: it evaluates any state in any language, automatically detects scripts and languages in sub-milliseconds, and dispatches to the optimal checkpoint in a single forward pass.
39
50
 
40
51
  ```python
41
52
  import laya
53
+ from laya import Router
42
54
 
43
- # 1. Load the fine-tuned model directly from Hugging Face Hub (auto-downloads weights)
44
- agent = laya.load("convaiinnovations/laya")
55
+ # Preload checkpoints into memory for instant sub-35ms routing
56
+ router = Router(preload=True)
45
57
 
46
- # 2. Provide any state (string or dictionary)
58
+ # 1. State in any language or schema
47
59
  state = {
48
60
  "from": "user@acme.com",
49
61
  "subject": "Duplicate charge on invoice #4411",
50
62
  "body": "Hi, we were billed twice for March. Please refund the duplicate today or we will cancel our plan."
51
63
  }
52
64
 
53
- # 3. Define your typed questions
65
+ # 2. Define your typed questions
54
66
  questions = {
55
- # choice: categorical selection with probabilities & confidence
56
67
  "department": {
57
68
  "type": "choice",
58
- "instructions": "Which department should handle this email?",
69
+ "instructions": "Which department should handle this request?",
59
70
  "criteria": {
60
71
  "billing": "invoices, payments, refunds",
61
72
  "technical": "bugs, outages, system errors",
@@ -63,38 +74,117 @@ questions = {
63
74
  "other": "everything else"
64
75
  }
65
76
  },
66
- # score: placement on an ordinal rubric
67
77
  "urgency": {
68
78
  "type": "score",
69
79
  "instructions": "How urgent is this request?",
70
80
  "criteria": ["not urgent", "soon", "critical deadline or blocking issue"]
71
81
  },
72
- # noul: calibrated boolean probability P(true)
73
82
  "churn_risk": {
74
83
  "type": "noul",
75
84
  "instructions": "Does the user threaten to cancel or leave?"
76
85
  },
77
- "is_phishing": {
86
+ "refund_requested": {
78
87
  "type": "noul",
79
- "instructions": "Is this email a phishing or scam attempt?"
88
+ "instructions": "Does the user explicitly request a refund?"
80
89
  }
81
90
  }
82
91
 
83
- # 4. Run all questions in ONE single forward pass (~35 ms on GPU)
84
- result = agent.predict(state, questions)
85
- answers = result["answers"]
92
+ # 3. English state -> automatically routed to laya (ModernBERT-large, 39.5 ms)
93
+ res_en = router.predict(state, questions)
94
+ print("Department :", res_en["answers"]["department"]["choice"]) # -> billing (confidence: 0.94)
95
+ print("Routing :", res_en["routing"]["model"]) # -> english
96
+
97
+ # 4. Hindi state -> automatically routed to laya-multilingual (mmBERT-base, 32.8 ms)
98
+ res_hi = router.predict({"body": "मुझसे दो बार शुल्क लिया गया, कृपया पैसे वापस करें।"}, questions)
99
+ print("Department :", res_hi["answers"]["department"]["choice"]) # -> billing (confidence: 0.86)
100
+ print("Routing :", res_hi["routing"]["model"]) # -> multilingual
101
+
102
+ # 5. Explicit override when you want a specific checkpoint
103
+ res_td = router.predict(state, questions, model="typed-decisions")
104
+ ```
105
+
106
+ Every result carries full routing metadata explaining why the choice was made:
107
+
108
+ ```python
109
+ res_hi["routing"]
110
+ # {
111
+ # 'model': 'multilingual',
112
+ # 'repo': 'convaiinnovations/laya/multilingual',
113
+ # 'reason': 'non-Latin script (devanagari, 100% of letters); the English checkpoint cannot read it'
114
+ # }
115
+ ```
116
+
117
+ Inspect a routing decision without running any forward pass:
118
+
119
+ ```python
120
+ router.route({"body": "Der Kunde wurde zweimal belastet"}, questions).reason
121
+ # "Latin script but language looks like 'de', not English"
122
+ ```
86
123
 
87
- print("Department :", answers["department"]["choice"])
88
- # -> billing (confidence: 0.94)
124
+ ### Why Route: The Evidence
89
125
 
90
- print("Urgency :", answers["urgency"]["score"])
91
- # -> 1.84 / 2.0
126
+ On a shared benchmark (17,416 questions, one T4 GPU, identical questions per model):
92
127
 
93
- print("Churn Risk :", answers["churn_risk"]["noul"])
94
- # -> 0.892 (89.2% probability)
128
+ | Benchmark / Task | English (`laya`) | Multilingual (`laya-multilingual`) | `Router` (Routed) |
129
+ |---|---|---|---|
130
+ | MASSIVE intent, English | **0.783** | 0.657 | **0.783** |
131
+ | MASSIVE intent, 13 other languages | 0.306 | **0.451** | **0.451** |
132
+ | XNLI, English | **0.860** | 0.843 | **0.860** |
133
+ | XNLI, 14 other languages | 0.521 | **0.731** | **0.731** |
134
+ | Languages usable (>3x random) | 23 / 51 | 45 / 51 | **45 / 51** |
135
+ | Latency, 1 question (T4 GPU) | 39.5 ms | **32.8 ms** | **32.8 ms** |
136
+ | Latency, 10 questions batched | 158.6 ms | **72.3 ms** | **72.3 ms** |
137
+
138
+ The English checkpoint collapses on non-Latin scripts (Khmer scores **0.000 accuracy at 0.952 confidence**). Because the model stays confident while being wrong, confidence gating cannot save you. `Router` detects the script in <0.5 ms pure Python before the forward pass.
139
+
140
+ ### Production Preload & Memory
95
141
 
96
- print("Phishing :", answers["is_phishing"]["noul"])
97
- # -> 0.008 (0.8% probability)
142
+ A cold checkpoint build costs seconds; language detection costs microseconds. At the default `max_loaded=1`, traffic that alternates languages rebuilds a model on *every* request (measured at a 7.4 s median reload on CPU and 10.3 s on T4).
143
+
144
+ For a server or production app, preload:
145
+
146
+ ```python
147
+ # Every checkpoint resident in memory; language flips cost detection only (<1 ms)
148
+ router = Router(preload=True)
149
+ router = Router(preload=True, device="cuda")
150
+
151
+ # Or preload only the specific checkpoints you serve:
152
+ router.preload(["english", "multilingual"])
153
+
154
+ # If your app already built an agent, attach it to avoid duplicate VRAM:
155
+ router.attach("english", existing_agent)
156
+
157
+ # Manage resident memory (default keeps 1 hot, LRU eviction)
158
+ router = Router(max_loaded=2) # keep two hot
159
+ router.unload() # free memory
160
+ ```
161
+
162
+ | Deployment Mode | Per-Request Latency | Model Reloads |
163
+ |---|---|---|
164
+ | `Router()` (lazy, `max_loaded=1`) | 7 to 10 s on every language switch | 1 per switch |
165
+ | `Router(preload=True)` | **32.8 ms (GPU) / 193–464 ms (CPU)** | **none** |
166
+
167
+ ---
168
+
169
+ ## Single-Model Mode (Direct SDK)
170
+
171
+ If you only need a single checkpoint for a dedicated pipeline, you can load models directly:
172
+
173
+ ```python
174
+ import laya
175
+
176
+ # 1. Load a specific checkpoint directly from the hub
177
+ agent = laya.load("convaiinnovations/laya") # English root
178
+ agent_ml = laya.load("convaiinnovations/laya", subfolder="multilingual") # 100+ languages
179
+ agent_td = laya.load("convaiinnovations/laya", subfolder="typed-decisions")
180
+
181
+ # 2. Run all questions in ONE single forward pass (~35 ms on GPU)
182
+ result = agent.predict(state, questions)
183
+ answers = result["answers"]
184
+
185
+ print("Department :", answers["department"]["choice"]) # -> billing (confidence: 0.94)
186
+ print("Urgency :", answers["urgency"]["score"]) # -> 1.84 / 2.0
187
+ print("Churn Risk :", answers["churn_risk"]["noul"]) # -> 0.892 (89.2% probability)
98
188
  ```
99
189
 
100
190
  ---
@@ -141,94 +231,6 @@ triage = agent.predict({"message": "My payment failed twice"}, laya.triage_quest
141
231
 
142
232
  ---
143
233
 
144
- ## Model Routing (three checkpoints, one call)
145
-
146
- Laya ships three checkpoints. `Router` picks the right one per request and loads it lazily.
147
-
148
- | name | repo | size | context | best at |
149
- |---|---|---|---|---|
150
- | `english` | [`convaiinnovations/laya`](https://huggingface.co/convaiinnovations/laya) | 421M | 512 | English text |
151
- | `multilingual` | [`convaiinnovations/laya-multilingual`](https://huggingface.co/convaiinnovations/laya-multilingual) | 322M | 1024 | 100+ languages, 2x faster |
152
- | `typed-decisions` | [`convaiinnovations/laya-typed-decisions`](https://huggingface.co/convaiinnovations/laya-typed-decisions) | 421M | 1024 | the four typed-decisions workflows |
153
-
154
- ```python
155
- from laya import Router
156
-
157
- router = Router() # nothing is downloaded until a request needs it
158
-
159
- # English -> routed to the English checkpoint
160
- router.predict({"body": "I was charged twice, please refund."}, questions)
161
-
162
- # Hindi -> routed to the multilingual checkpoint automatically
163
- router.predict({"body": "मुझसे दो बार शुल्क लिया गया"}, questions)
164
-
165
- # explicit when you already know
166
- router.predict(state, questions, model="typed-decisions")
167
- router.predict(state, questions, lang="de")
168
- ```
169
-
170
- Every result carries the decision that produced it:
171
-
172
- ```python
173
- result = router.predict({"body": "二重に請求されました"}, questions)
174
- result["routing"]
175
- # {'model': 'multilingual',
176
- # 'repo': 'convaiinnovations/laya-multilingual',
177
- # 'reason': 'non-Latin script (kana, 100% of letters); the English checkpoint cannot read it',
178
- # ...}
179
- ```
180
-
181
- Inspect a decision without running the model:
182
-
183
- ```python
184
- router.route({"body": "Der Kunde wurde zweimal belastet"}, questions).reason
185
- # "Latin script but language looks like 'de', not English"
186
- ```
187
-
188
- ### Why route at all
189
-
190
- Accuracy on a shared benchmark (17,416 questions, one T4, identical questions per model):
191
-
192
- | | `english` | `multilingual` |
193
- |---|---|---|
194
- | MASSIVE intent, English | **0.783** | 0.657 |
195
- | MASSIVE intent, 13 other languages | 0.306 | **0.451** |
196
- | XNLI, English | **0.860** | 0.843 |
197
- | XNLI, 14 other languages | 0.521 | **0.731** |
198
- | English-only suites | **0.684** | 0.619 |
199
- | Latency, 10 questions | 159 ms | **72 ms** |
200
-
201
- The English checkpoint does not degrade gracefully outside English -- it collapses, and stays
202
- confident while doing so. On 20-option MASSIVE intent (random = 0.050) it scores 0.100 on Hindi
203
- and 0.103 on Korean, with an expected calibration error of 0.855. Script detection is therefore
204
- the primary routing signal.
205
-
206
- ### Routing rules
207
-
208
- Precedence, highest first:
209
-
210
- 1. `model=` -- explicit checkpoint.
211
- 2. `task="typed_decisions"` -- explicit task.
212
- 3. A question-id set exactly matching a typed-decisions workflow, **only** if you construct the
213
- router with `auto_task_detection=True`. It is off by default: that checkpoint is fine-tuned on
214
- four synthetic workflows and should not be a silent fallback.
215
- 4. `lang=` -- explicit language code.
216
- 5. Detected script (exact) and, for Latin text, a stopword/diacritic language guess (best effort).
217
- 6. `default=` (`"english"` unless you change it).
218
-
219
- ### Memory
220
-
221
- All three together are ~1.16B parameters, so `Router` keeps **one** resident by default and
222
- evicts least-recently-used:
223
-
224
- ```python
225
- Router(max_loaded=2) # keep two hot
226
- router.unload() # free everything
227
- router.loaded # ['multilingual']
228
- ```
229
-
230
- ---
231
-
232
234
  ## Decision Primitives
233
235
 
234
236
  | Primitive | Output | Use Cases |
@@ -277,7 +279,8 @@ published, never measured here** (no TypeSafe API access), so sample sizes and p
277
279
  | typed-decisions, 2,000 decisions | 0.727 | **0.766** | +0.039 |
278
280
  | AG News, 4 labels | 0.910 | **0.950** | +0.040 |
279
281
  | DAIR Emotion, 6 labels | 0.480 | **0.595** | +0.115 |
280
- | ECE *(lower better)* | 0.246 | **0.081** | 3× better |
282
+ | Banking77 (72 vs 77 labels) | **0.870** | 0.425 | Jev leads on >20 options |
283
+ | ECE *(lower better)* | 0.246 | **0.081** | 3× better (post-temperature) |
281
284
  | p50 latency, 1 question | 236–276 ms | **32.8 ms** | 7.8× faster |
282
285
  | Languages usable | *no published benchmark* | **45 of 51** | — |
283
286
  | Weights | closed API | **Apache 2.0** | — |
@@ -286,6 +289,12 @@ published, never measured here** (no TypeSafe API access), so sample sizes and p
286
289
  On DAIR Emotion, Jev assigned **zero probability to the true label on 16% of examples** — a hard
287
290
  failure for anything branching on confidence.
288
291
 
292
+ #### Where Jev leads
293
+
294
+ * **High-cardinality label spaces (>20 options at default settings):** On Banking77, Jev scores 0.870 (on 72 labels) while Laya scores 0.425 (on 77 labels at default 256-token head budget). This is an architectural token-budget constraint: options share a fixed `head_max_len` budget (192 tokens on English, 256 on multilingual), so 77 options receive only ~3 to 4 tokens per label, causing text to become indistinguishable. Jev supports up to 255 options out-of-the-box. While `laya-multilingual` supports 1,024 context (and up to 8,192 in the encoder) and you can raise `agent.cfg["head_max_len"] = 512` at runtime, Jev is currently better suited for 50+ options in a single prompt without tuning.
295
+ * **Soft distribution matching:** On typed-decisions, while Laya achieves higher argmax accuracy (0.766 vs 0.727), Jev achieves higher soft accuracy (0.580 vs 0.471) against the teacher's full probability distributions.
296
+ * **Out-of-the-box raw calibration:** Before temperature scaling, the base checkpoint has higher raw ECE (0.213 vs 0.144). Laya achieves its 0.081 ECE after domain temperature fitting.
297
+
289
298
  Full detail, including every workflow and all 51 languages: **[`BENCHMARKS.md`](BENCHMARKS.md)**.
290
299
 
291
300
  ### typed-decisions, measured on all three checkpoints
@@ -352,11 +361,12 @@ temperatures at all, so fit them before relying on its probabilities.
352
361
  against a 0.318 random baseline and a 0.461 majority-class baseline. The 0.766 figure comes
353
362
  from the checkpoint fine-tuned on that benchmark's own training split. Laya is a fast base to
354
363
  specialise, not a zero-shot decision engine.
355
- * **Keep `choice` questions under ~20 options.** Every option is rendered into a fixed
356
- `head_max_len` budget (192 tokens on `laya`, 256 on the others), so a 77-option question
357
- leaves roughly 4 tokens per label and the option text stops being distinguishable —
358
- accuracy falls off sharply. Split large label spaces into a coarse choice followed by a
359
- fine one.
364
+ * **High-cardinality choice questions and token budgets:** Sequences split into an option prompt budget (`head_max_len`) and the remaining document/state budget (`max_len - head_max_len`):
365
+ * `laya` (English) defaults to 512 context (`head_max_len = 192`, ~320 tokens for state).
366
+ * `laya-multilingual` and `laya-typed-decisions` default to 1,024 context (`head_max_len = 256`, ~768 tokens for state; mmBERT-base encoder supports up to 8,192 with RoPE).
367
+ At default settings, a 77-option question like Banking77 allocates only `(256 - 16) // 77` ≈ 3–4 tokens per label, which causes accuracy to fall off sharply (0.425 vs Jev's 0.870). If evaluating 50+ options in a single question:
368
+ 1. Raise `agent.cfg["head_max_len"] = 512` and `agent.cfg["max_len"] = 1024` (or up to 2048 / 4096 / 8192) so every option has enough tokens to remain distinct.
369
+ 2. Or split large option sets into a two-step coarse-to-fine hierarchical choice.
360
370
  * Ordinal `score` questions are the weakest primitive (SST-5 0.372).
361
371
  * `laya` collapses outside English; `laya-multilingual` is weaker on English. Route, or pick
362
372
  deliberately.
@@ -22,7 +22,7 @@ from .presets import (
22
22
  )
23
23
  from .router import DEFAULT_MODELS, RouteDecision, Router
24
24
 
25
- __version__ = "0.3.2"
25
+ __version__ = "0.3.4"
26
26
  __all__ = [
27
27
  "Agent",
28
28
  "RLAgent",
@@ -201,17 +201,31 @@ class Agent:
201
201
  self.dtype = torch.float32
202
202
 
203
203
  # 2. Place on device with graceful fallback to CPU on memory error
204
+ fell_back_from = fell_back_why = None
204
205
  try:
205
206
  self.model.to(self.device).eval()
206
207
  except (RuntimeError, torch.cuda.OutOfMemoryError) as e:
207
208
  if self.device.type != "cpu":
208
- print(f"Warning: Could not place model on {self.device} ({e}). Falling back to CPU.")
209
+ # Record what actually went wrong: the reason matters more than the symptom,
210
+ # and it is the only place the underlying exception is ever surfaced.
211
+ fell_back_from, fell_back_why = self.device, e
209
212
  self.device = torch.device("cpu")
210
213
  self.dtype = torch.float32
211
214
  self.model.to(self.device).eval()
212
215
  else:
213
216
  raise e
214
217
 
218
+ if fell_back_from is not None:
219
+ print(
220
+ "\n[laya] Warning: could not place the model on %s, so it is running on CPU.\n"
221
+ " Reason: %s\n"
222
+ " Inference will be roughly 10-15x slower (~200-500 ms rather than ~35 ms).\n"
223
+ " If this is a newer NVIDIA GPU (Blackwell / RTX 50-series), your PyTorch build\n"
224
+ " may not support its CUDA architecture:\n"
225
+ " pip install --pre torch --index-url https://download.pytorch.org/whl/nightly/cu128\n"
226
+ " See https://pytorch.org/get-started/locally/\n"
227
+ % (fell_back_from, fell_back_why), flush=True)
228
+
215
229
  @staticmethod
216
230
  def _to_internal(qdef: Dict) -> Dict:
217
231
  t = qdef["type"]
@@ -194,6 +194,19 @@ class Router:
194
194
  if k not in self._order:
195
195
  self._agents.pop(k, None)
196
196
 
197
+ def attach(self, name: str, agent: Any):
198
+ """Register an already-built Agent under `name` instead of loading a second copy.
199
+
200
+ Useful when the process has a checkpoint loaded for other reasons: a demo that already
201
+ built `convaiinnovations/laya` can hand it to the router rather than pay for -- and hold
202
+ in memory -- a duplicate 421M parameters.
203
+ """
204
+ key = normalise_name(name)
205
+ self._agents[key] = agent
206
+ self._touch(key)
207
+ self.max_loaded = max(self.max_loaded, len(self._agents))
208
+ return agent
209
+
197
210
  def preload(self, names: Optional[List[str]] = None):
198
211
  """Download and build checkpoints up front so no request ever pays a model load.
199
212
 
@@ -203,9 +216,10 @@ class Router:
203
216
  the LRU would immediately evict what this just built.
204
217
  """
205
218
  names = [normalise_name(n) for n in (names or list(self.models))]
206
- self.max_loaded = max(self.max_loaded, len(names))
219
+ self.max_loaded = max(self.max_loaded, len(names), len(self._agents))
207
220
  for n in names:
208
- self.load(n)
221
+ if n not in self._agents: # an attached agent is already built
222
+ self.load(n)
209
223
  return self
210
224
 
211
225
  def unload(self, name: Optional[str] = None):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: laya
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Fast, non-autoregressive System 1 decision engine with calibrated probabilities
5
5
  Author: Convai Innovations
6
6
  License: Apache-2.0
@@ -28,10 +28,17 @@ Requires-Dist: numpy>=1.20.0
28
28
  Dynamic: license-file
29
29
  Dynamic: requires-python
30
30
 
31
- # Laya
31
+ <p align="center">
32
+ <picture>
33
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/logo-lockup-dark.png" />
34
+ <img src="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/logo-lockup.png" alt="Laya" width="330" />
35
+ </picture>
36
+ </p>
32
37
 
33
38
  **Multilingual, non-autoregressive System 1 decision engine.** Typed decisions over 100+ languages in a single forward pass — 33 ms — trained with reinforcement learning against strictly proper scoring rules (RLCD), with a router that picks the right checkpoint per request.
34
39
 
40
+ <div align="center">
41
+
35
42
  [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/15d4Yv__KHeHjshVb-6PRTfqVllxih2S3?usp=sharing)
36
43
  [![PyPI version](https://img.shields.io/pypi/v/laya.svg)](https://pypi.org/project/laya/)
37
44
  [![Hugging Face Model](https://img.shields.io/badge/%F0%9F%A4%97%20Model-convaiinnovations%2Flaya-blue)](https://huggingface.co/convaiinnovations/laya)
@@ -41,8 +48,10 @@ Dynamic: requires-python
41
48
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-nandakishorm-FFDD00?logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/nandakishorm)
42
49
  [![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](https://opensource.org/licenses/Apache-2.0)
43
50
 
51
+ </div>
52
+
44
53
  <p align="center">
45
- <img src="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/laya_vs_jev.png" alt="Laya with routing versus TypeSafe Jev: accuracy on shared public datasets, language coverage, speed and calibration" width="100%" />
54
+ <img src="https://raw.githubusercontent.com/NandhaKishorM/laya/main/assets/laya_vs_jev_full.png" alt="Laya versus TypeSafe Jev: accuracy on shared public datasets, every application workflow, all 51 languages, speed, calibration, and the cost of not preloading" width="100%" />
46
55
  </p>
47
56
 
48
57
  Laya evaluates typed questions (`choice`, `score`, `noul`) over any state (text, email, ticket or JSON document) in **a single forward pass** — 33 ms for one question, 7.2 ms/question batched, measured on a T4. No text generation, so nothing to parse and nothing to hallucinate.
@@ -65,27 +74,29 @@ pip install laya
65
74
 
66
75
  ---
67
76
 
68
- ## Quickstart
77
+ ## Quickstart: Route Mode (Recommended)
78
+
79
+ Laya ships three checkpoints. The built-in **`Router`** is the recommended entry point: it evaluates any state in any language, automatically detects scripts and languages in sub-milliseconds, and dispatches to the optimal checkpoint in a single forward pass.
69
80
 
70
81
  ```python
71
82
  import laya
83
+ from laya import Router
72
84
 
73
- # 1. Load the fine-tuned model directly from Hugging Face Hub (auto-downloads weights)
74
- agent = laya.load("convaiinnovations/laya")
85
+ # Preload checkpoints into memory for instant sub-35ms routing
86
+ router = Router(preload=True)
75
87
 
76
- # 2. Provide any state (string or dictionary)
88
+ # 1. State in any language or schema
77
89
  state = {
78
90
  "from": "user@acme.com",
79
91
  "subject": "Duplicate charge on invoice #4411",
80
92
  "body": "Hi, we were billed twice for March. Please refund the duplicate today or we will cancel our plan."
81
93
  }
82
94
 
83
- # 3. Define your typed questions
95
+ # 2. Define your typed questions
84
96
  questions = {
85
- # choice: categorical selection with probabilities & confidence
86
97
  "department": {
87
98
  "type": "choice",
88
- "instructions": "Which department should handle this email?",
99
+ "instructions": "Which department should handle this request?",
89
100
  "criteria": {
90
101
  "billing": "invoices, payments, refunds",
91
102
  "technical": "bugs, outages, system errors",
@@ -93,38 +104,117 @@ questions = {
93
104
  "other": "everything else"
94
105
  }
95
106
  },
96
- # score: placement on an ordinal rubric
97
107
  "urgency": {
98
108
  "type": "score",
99
109
  "instructions": "How urgent is this request?",
100
110
  "criteria": ["not urgent", "soon", "critical deadline or blocking issue"]
101
111
  },
102
- # noul: calibrated boolean probability P(true)
103
112
  "churn_risk": {
104
113
  "type": "noul",
105
114
  "instructions": "Does the user threaten to cancel or leave?"
106
115
  },
107
- "is_phishing": {
116
+ "refund_requested": {
108
117
  "type": "noul",
109
- "instructions": "Is this email a phishing or scam attempt?"
118
+ "instructions": "Does the user explicitly request a refund?"
110
119
  }
111
120
  }
112
121
 
113
- # 4. Run all questions in ONE single forward pass (~35 ms on GPU)
114
- result = agent.predict(state, questions)
115
- answers = result["answers"]
122
+ # 3. English state -> automatically routed to laya (ModernBERT-large, 39.5 ms)
123
+ res_en = router.predict(state, questions)
124
+ print("Department :", res_en["answers"]["department"]["choice"]) # -> billing (confidence: 0.94)
125
+ print("Routing :", res_en["routing"]["model"]) # -> english
126
+
127
+ # 4. Hindi state -> automatically routed to laya-multilingual (mmBERT-base, 32.8 ms)
128
+ res_hi = router.predict({"body": "मुझसे दो बार शुल्क लिया गया, कृपया पैसे वापस करें।"}, questions)
129
+ print("Department :", res_hi["answers"]["department"]["choice"]) # -> billing (confidence: 0.86)
130
+ print("Routing :", res_hi["routing"]["model"]) # -> multilingual
131
+
132
+ # 5. Explicit override when you want a specific checkpoint
133
+ res_td = router.predict(state, questions, model="typed-decisions")
134
+ ```
135
+
136
+ Every result carries full routing metadata explaining why the choice was made:
137
+
138
+ ```python
139
+ res_hi["routing"]
140
+ # {
141
+ # 'model': 'multilingual',
142
+ # 'repo': 'convaiinnovations/laya/multilingual',
143
+ # 'reason': 'non-Latin script (devanagari, 100% of letters); the English checkpoint cannot read it'
144
+ # }
145
+ ```
146
+
147
+ Inspect a routing decision without running any forward pass:
148
+
149
+ ```python
150
+ router.route({"body": "Der Kunde wurde zweimal belastet"}, questions).reason
151
+ # "Latin script but language looks like 'de', not English"
152
+ ```
116
153
 
117
- print("Department :", answers["department"]["choice"])
118
- # -> billing (confidence: 0.94)
154
+ ### Why Route: The Evidence
119
155
 
120
- print("Urgency :", answers["urgency"]["score"])
121
- # -> 1.84 / 2.0
156
+ On a shared benchmark (17,416 questions, one T4 GPU, identical questions per model):
122
157
 
123
- print("Churn Risk :", answers["churn_risk"]["noul"])
124
- # -> 0.892 (89.2% probability)
158
+ | Benchmark / Task | English (`laya`) | Multilingual (`laya-multilingual`) | `Router` (Routed) |
159
+ |---|---|---|---|
160
+ | MASSIVE intent, English | **0.783** | 0.657 | **0.783** |
161
+ | MASSIVE intent, 13 other languages | 0.306 | **0.451** | **0.451** |
162
+ | XNLI, English | **0.860** | 0.843 | **0.860** |
163
+ | XNLI, 14 other languages | 0.521 | **0.731** | **0.731** |
164
+ | Languages usable (>3x random) | 23 / 51 | 45 / 51 | **45 / 51** |
165
+ | Latency, 1 question (T4 GPU) | 39.5 ms | **32.8 ms** | **32.8 ms** |
166
+ | Latency, 10 questions batched | 158.6 ms | **72.3 ms** | **72.3 ms** |
167
+
168
+ The English checkpoint collapses on non-Latin scripts (Khmer scores **0.000 accuracy at 0.952 confidence**). Because the model stays confident while being wrong, confidence gating cannot save you. `Router` detects the script in <0.5 ms pure Python before the forward pass.
169
+
170
+ ### Production Preload & Memory
125
171
 
126
- print("Phishing :", answers["is_phishing"]["noul"])
127
- # -> 0.008 (0.8% probability)
172
+ A cold checkpoint build costs seconds; language detection costs microseconds. At the default `max_loaded=1`, traffic that alternates languages rebuilds a model on *every* request (measured at a 7.4 s median reload on CPU and 10.3 s on T4).
173
+
174
+ For a server or production app, preload:
175
+
176
+ ```python
177
+ # Every checkpoint resident in memory; language flips cost detection only (<1 ms)
178
+ router = Router(preload=True)
179
+ router = Router(preload=True, device="cuda")
180
+
181
+ # Or preload only the specific checkpoints you serve:
182
+ router.preload(["english", "multilingual"])
183
+
184
+ # If your app already built an agent, attach it to avoid duplicate VRAM:
185
+ router.attach("english", existing_agent)
186
+
187
+ # Manage resident memory (default keeps 1 hot, LRU eviction)
188
+ router = Router(max_loaded=2) # keep two hot
189
+ router.unload() # free memory
190
+ ```
191
+
192
+ | Deployment Mode | Per-Request Latency | Model Reloads |
193
+ |---|---|---|
194
+ | `Router()` (lazy, `max_loaded=1`) | 7 to 10 s on every language switch | 1 per switch |
195
+ | `Router(preload=True)` | **32.8 ms (GPU) / 193–464 ms (CPU)** | **none** |
196
+
197
+ ---
198
+
199
+ ## Single-Model Mode (Direct SDK)
200
+
201
+ If you only need a single checkpoint for a dedicated pipeline, you can load models directly:
202
+
203
+ ```python
204
+ import laya
205
+
206
+ # 1. Load a specific checkpoint directly from the hub
207
+ agent = laya.load("convaiinnovations/laya") # English root
208
+ agent_ml = laya.load("convaiinnovations/laya", subfolder="multilingual") # 100+ languages
209
+ agent_td = laya.load("convaiinnovations/laya", subfolder="typed-decisions")
210
+
211
+ # 2. Run all questions in ONE single forward pass (~35 ms on GPU)
212
+ result = agent.predict(state, questions)
213
+ answers = result["answers"]
214
+
215
+ print("Department :", answers["department"]["choice"]) # -> billing (confidence: 0.94)
216
+ print("Urgency :", answers["urgency"]["score"]) # -> 1.84 / 2.0
217
+ print("Churn Risk :", answers["churn_risk"]["noul"]) # -> 0.892 (89.2% probability)
128
218
  ```
129
219
 
130
220
  ---
@@ -171,94 +261,6 @@ triage = agent.predict({"message": "My payment failed twice"}, laya.triage_quest
171
261
 
172
262
  ---
173
263
 
174
- ## Model Routing (three checkpoints, one call)
175
-
176
- Laya ships three checkpoints. `Router` picks the right one per request and loads it lazily.
177
-
178
- | name | repo | size | context | best at |
179
- |---|---|---|---|---|
180
- | `english` | [`convaiinnovations/laya`](https://huggingface.co/convaiinnovations/laya) | 421M | 512 | English text |
181
- | `multilingual` | [`convaiinnovations/laya-multilingual`](https://huggingface.co/convaiinnovations/laya-multilingual) | 322M | 1024 | 100+ languages, 2x faster |
182
- | `typed-decisions` | [`convaiinnovations/laya-typed-decisions`](https://huggingface.co/convaiinnovations/laya-typed-decisions) | 421M | 1024 | the four typed-decisions workflows |
183
-
184
- ```python
185
- from laya import Router
186
-
187
- router = Router() # nothing is downloaded until a request needs it
188
-
189
- # English -> routed to the English checkpoint
190
- router.predict({"body": "I was charged twice, please refund."}, questions)
191
-
192
- # Hindi -> routed to the multilingual checkpoint automatically
193
- router.predict({"body": "मुझसे दो बार शुल्क लिया गया"}, questions)
194
-
195
- # explicit when you already know
196
- router.predict(state, questions, model="typed-decisions")
197
- router.predict(state, questions, lang="de")
198
- ```
199
-
200
- Every result carries the decision that produced it:
201
-
202
- ```python
203
- result = router.predict({"body": "二重に請求されました"}, questions)
204
- result["routing"]
205
- # {'model': 'multilingual',
206
- # 'repo': 'convaiinnovations/laya-multilingual',
207
- # 'reason': 'non-Latin script (kana, 100% of letters); the English checkpoint cannot read it',
208
- # ...}
209
- ```
210
-
211
- Inspect a decision without running the model:
212
-
213
- ```python
214
- router.route({"body": "Der Kunde wurde zweimal belastet"}, questions).reason
215
- # "Latin script but language looks like 'de', not English"
216
- ```
217
-
218
- ### Why route at all
219
-
220
- Accuracy on a shared benchmark (17,416 questions, one T4, identical questions per model):
221
-
222
- | | `english` | `multilingual` |
223
- |---|---|---|
224
- | MASSIVE intent, English | **0.783** | 0.657 |
225
- | MASSIVE intent, 13 other languages | 0.306 | **0.451** |
226
- | XNLI, English | **0.860** | 0.843 |
227
- | XNLI, 14 other languages | 0.521 | **0.731** |
228
- | English-only suites | **0.684** | 0.619 |
229
- | Latency, 10 questions | 159 ms | **72 ms** |
230
-
231
- The English checkpoint does not degrade gracefully outside English -- it collapses, and stays
232
- confident while doing so. On 20-option MASSIVE intent (random = 0.050) it scores 0.100 on Hindi
233
- and 0.103 on Korean, with an expected calibration error of 0.855. Script detection is therefore
234
- the primary routing signal.
235
-
236
- ### Routing rules
237
-
238
- Precedence, highest first:
239
-
240
- 1. `model=` -- explicit checkpoint.
241
- 2. `task="typed_decisions"` -- explicit task.
242
- 3. A question-id set exactly matching a typed-decisions workflow, **only** if you construct the
243
- router with `auto_task_detection=True`. It is off by default: that checkpoint is fine-tuned on
244
- four synthetic workflows and should not be a silent fallback.
245
- 4. `lang=` -- explicit language code.
246
- 5. Detected script (exact) and, for Latin text, a stopword/diacritic language guess (best effort).
247
- 6. `default=` (`"english"` unless you change it).
248
-
249
- ### Memory
250
-
251
- All three together are ~1.16B parameters, so `Router` keeps **one** resident by default and
252
- evicts least-recently-used:
253
-
254
- ```python
255
- Router(max_loaded=2) # keep two hot
256
- router.unload() # free everything
257
- router.loaded # ['multilingual']
258
- ```
259
-
260
- ---
261
-
262
264
  ## Decision Primitives
263
265
 
264
266
  | Primitive | Output | Use Cases |
@@ -307,7 +309,8 @@ published, never measured here** (no TypeSafe API access), so sample sizes and p
307
309
  | typed-decisions, 2,000 decisions | 0.727 | **0.766** | +0.039 |
308
310
  | AG News, 4 labels | 0.910 | **0.950** | +0.040 |
309
311
  | DAIR Emotion, 6 labels | 0.480 | **0.595** | +0.115 |
310
- | ECE *(lower better)* | 0.246 | **0.081** | 3× better |
312
+ | Banking77 (72 vs 77 labels) | **0.870** | 0.425 | Jev leads on >20 options |
313
+ | ECE *(lower better)* | 0.246 | **0.081** | 3× better (post-temperature) |
311
314
  | p50 latency, 1 question | 236–276 ms | **32.8 ms** | 7.8× faster |
312
315
  | Languages usable | *no published benchmark* | **45 of 51** | — |
313
316
  | Weights | closed API | **Apache 2.0** | — |
@@ -316,6 +319,12 @@ published, never measured here** (no TypeSafe API access), so sample sizes and p
316
319
  On DAIR Emotion, Jev assigned **zero probability to the true label on 16% of examples** — a hard
317
320
  failure for anything branching on confidence.
318
321
 
322
+ #### Where Jev leads
323
+
324
+ * **High-cardinality label spaces (>20 options at default settings):** On Banking77, Jev scores 0.870 (on 72 labels) while Laya scores 0.425 (on 77 labels at default 256-token head budget). This is an architectural token-budget constraint: options share a fixed `head_max_len` budget (192 tokens on English, 256 on multilingual), so 77 options receive only ~3 to 4 tokens per label, causing text to become indistinguishable. Jev supports up to 255 options out-of-the-box. While `laya-multilingual` supports 1,024 context (and up to 8,192 in the encoder) and you can raise `agent.cfg["head_max_len"] = 512` at runtime, Jev is currently better suited for 50+ options in a single prompt without tuning.
325
+ * **Soft distribution matching:** On typed-decisions, while Laya achieves higher argmax accuracy (0.766 vs 0.727), Jev achieves higher soft accuracy (0.580 vs 0.471) against the teacher's full probability distributions.
326
+ * **Out-of-the-box raw calibration:** Before temperature scaling, the base checkpoint has higher raw ECE (0.213 vs 0.144). Laya achieves its 0.081 ECE after domain temperature fitting.
327
+
319
328
  Full detail, including every workflow and all 51 languages: **[`BENCHMARKS.md`](BENCHMARKS.md)**.
320
329
 
321
330
  ### typed-decisions, measured on all three checkpoints
@@ -382,11 +391,12 @@ temperatures at all, so fit them before relying on its probabilities.
382
391
  against a 0.318 random baseline and a 0.461 majority-class baseline. The 0.766 figure comes
383
392
  from the checkpoint fine-tuned on that benchmark's own training split. Laya is a fast base to
384
393
  specialise, not a zero-shot decision engine.
385
- * **Keep `choice` questions under ~20 options.** Every option is rendered into a fixed
386
- `head_max_len` budget (192 tokens on `laya`, 256 on the others), so a 77-option question
387
- leaves roughly 4 tokens per label and the option text stops being distinguishable —
388
- accuracy falls off sharply. Split large label spaces into a coarse choice followed by a
389
- fine one.
394
+ * **High-cardinality choice questions and token budgets:** Sequences split into an option prompt budget (`head_max_len`) and the remaining document/state budget (`max_len - head_max_len`):
395
+ * `laya` (English) defaults to 512 context (`head_max_len = 192`, ~320 tokens for state).
396
+ * `laya-multilingual` and `laya-typed-decisions` default to 1,024 context (`head_max_len = 256`, ~768 tokens for state; mmBERT-base encoder supports up to 8,192 with RoPE).
397
+ At default settings, a 77-option question like Banking77 allocates only `(256 - 16) // 77` ≈ 3–4 tokens per label, which causes accuracy to fall off sharply (0.425 vs Jev's 0.870). If evaluating 50+ options in a single question:
398
+ 1. Raise `agent.cfg["head_max_len"] = 512` and `agent.cfg["max_len"] = 1024` (or up to 2048 / 4096 / 8192) so every option has enough tokens to remain distinct.
399
+ 2. Or split large option sets into a two-step coarse-to-fine hierarchical choice.
390
400
  * Ordinal `score` questions are the weakest primitive (SST-5 0.372).
391
401
  * `laya` collapses outside English; `laya-multilingual` is weaker on English. Route, or pick
392
402
  deliberately.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "laya"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "Fast, non-autoregressive System 1 decision engine with calibrated probabilities"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="laya",
5
- version="0.3.2",
5
+ version="0.3.4",
6
6
  description="Fast, non-autoregressive System 1 decision engine with calibrated probabilities",
7
7
  long_description=open("README.md").read(),
8
8
  long_description_content_type="text/markdown",
@@ -99,6 +99,23 @@ parsed = json.loads(render_options(
99
99
  {"t": "noul", "ins": "x", "crit": {"true": {"a": 1}, "false": {"b": 2}}})[1].split("true: ", 1)[1])
100
100
  check("emitted json round-trips", parsed, {"a": 1})
101
101
 
102
+
103
+ # --------------------------------------------------------------- CPU-fallback warning (#9 follow-up)
104
+ # The warning must fire only when a fallback actually happened -- not merely because the machine
105
+ # has CUDA. `laya.load(path, device="cpu")` on a GPU box is a deliberate choice, not a problem.
106
+ import inspect # noqa: E402
107
+
108
+ from laya import agent as _agent # noqa: E402
109
+
110
+ _src = inspect.getsource(_agent.Agent.__init__)
111
+ check_true("fallback/flag is initialised", "fell_back_from = fell_back_why = None" in _src)
112
+ check_true("fallback/warns only on a real fallback", "if fell_back_from is not None:" in _src)
113
+ check_true("fallback/reports the underlying reason", "Reason: %s" in _src)
114
+ check_true("fallback/keeps the actionable advice", "download.pytorch.org/whl/nightly" in _src)
115
+ check_true("fallback/no bare cuda probe for the warning",
116
+ "torch.cuda.is_available() or getattr(torch.version" not in _src)
117
+
118
+
102
119
  print("\n%d passed, %d failed" % (len(PASS), len(FAIL)))
103
120
  for f in FAIL:
104
121
  print(" FAIL " + f)
@@ -255,6 +255,21 @@ _load_stub(rp2, "english")
255
255
  check("preload/touch does not evict", sorted(rp2.loaded), ["english", "multilingual"])
256
256
 
257
257
 
258
+ # --------------------------------------------------------------------- attach
259
+ ra = stubbed_router(1)
260
+ sentinel = _Stub("already-built")
261
+ ra.attach("english", sentinel)
262
+ check("attach/registers under the name", ra._agents["english"], sentinel)
263
+ check("attach/counts as resident", "english" in ra.loaded, True)
264
+ check("attach/raises max_loaded to hold it", ra.max_loaded >= 1, True)
265
+ # attaching then loading another must not evict the attached one
266
+ ra.max_loaded = max(ra.max_loaded, 2)
267
+ _load_stub(ra, "multilingual")
268
+ check("attach/survives a later load", sorted(ra.loaded), ["english", "multilingual"])
269
+ check("attach/still the same object", ra._agents["english"] is sentinel, True)
270
+ check("attach/accepts aliases", stubbed_router(1).attach("en", _Stub("x")) is not None, True)
271
+
272
+
258
273
  # --------------------------------------------------------------------- report
259
274
  print("\n%d passed, %d failed" % (len(PASS), len(FAIL)))
260
275
  for f in FAIL:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes