Text Classification
Transformers
Safetensors
Russian
English
qwen3_5_text
text-generation
typed-decisions
system-one
russian
routing
agents
decider
Instructions to use smolnikov/migom-4b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use smolnikov/migom-4b with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="smolnikov/migom-4b")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("smolnikov/migom-4b") model = AutoModelForCausalLM.from_pretrained("smolnikov/migom-4b", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Migom 4B v0.1
Browse files- .gitattributes +1 -0
- LICENSE +176 -0
- NOTICE +8 -0
- README.md +100 -0
- chat_template.jinja +154 -0
- config.json +83 -0
- decider_config.json +21 -0
- generation_config.json +6 -0
- model.safetensors +3 -0
- server/decide_server.py +122 -0
- tokenizer.json +3 -0
- tokenizer_config.json +32 -0
- training/bench.py +50 -0
- training/build_public.py +305 -0
- training/build_skills_mix.py +77 -0
- training/gen_skills.py +144 -0
- training/to_jevk5_pub.py +24 -0
- training/train_decider_lora.py +114 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Migom (migom-4b)
|
| 2 |
+
Copyright 2026 Kirill Smolnikov / CapyAgent
|
| 3 |
+
|
| 4 |
+
This model is a fine-tuned derivative of:
|
| 5 |
+
- decider-4b (Mapika/decider-4b, revision eb5fbdfc), Apache License 2.0
|
| 6 |
+
- Qwen3.5-4B-Base (Qwen/Qwen3.5-4B-Base), Apache License 2.0
|
| 7 |
+
Modifications: LoRA fine-tuning (merged into the weights) on an open, commercially licensed
|
| 8 |
+
Russian/English typed-decision mix and synthetic skill-routing data (see README).
|
README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language:
|
| 3 |
+
- ru
|
| 4 |
+
- en
|
| 5 |
+
license: apache-2.0
|
| 6 |
+
library_name: transformers
|
| 7 |
+
pipeline_tag: text-classification
|
| 8 |
+
tags:
|
| 9 |
+
- typed-decisions
|
| 10 |
+
- system-one
|
| 11 |
+
- russian
|
| 12 |
+
- routing
|
| 13 |
+
- agents
|
| 14 |
+
- decider
|
| 15 |
+
base_model: Mapika/decider-4b
|
| 16 |
+
datasets:
|
| 17 |
+
- smolnikov/RuDecide
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
# Мигом 4B (migom-4b)
|
| 21 |
+
|
| 22 |
+
Собственная модель для быстрых решений, обученная нами на наших данных. Разработка Smolnikov / CapyAgent.
|
| 23 |
+
Старшая модель семейства. Младшие: [Мигом 2B](https://huggingface.co/smolnikov/migom-2b) и [Кивок 0.3B](https://huggingface.co/smolnikov/kivok-0.3b).
|
| 24 |
+
Работает локально, без облака: около 0,05 с на решение на ноутбучной RTX 5070 Ti (веса bf16 8,4 ГБ, нужна видеокарта от 12 ГБ).
|
| 25 |
+
|
| 26 |
+
Модель не пишет текст. Она получает ситуацию (`state`) и один или несколько типизированных вопросов и за один проход
|
| 27 |
+
возвращает распределение вероятностей по вариантам:
|
| 28 |
+
- `choice` - выбрать вариант из списка с описаниями;
|
| 29 |
+
- `score` - оценка по порядковой шкале (до 10 уровней);
|
| 30 |
+
- `noul` - да/нет с вероятностью.
|
| 31 |
+
|
| 32 |
+
Для чего: выбрать навык или инструмент агента (включая «навык не нужен»), разобрать обращение в поддержку,
|
| 33 |
+
заметить атаку на ассистента, отфильтровать спам, понять намерение пользователя, ответить на вопрос с выбором
|
| 34 |
+
вариантов, где нужны рассуждение и знания о мире. Вероятности откалиброваны (температура по типу вопроса).
|
| 35 |
+
|
| 36 |
+
## Запуск
|
| 37 |
+
|
| 38 |
+
```python
|
| 39 |
+
# pip install decider-ai==1.4.0 transformers peft
|
| 40 |
+
from decider.infer import Decider
|
| 41 |
+
from huggingface_hub import snapshot_download
|
| 42 |
+
|
| 43 |
+
m = Decider(snapshot_download("smolnikov/migom-4b"), device="cuda") # или "cpu"
|
| 44 |
+
out = m.system_one(
|
| 45 |
+
"Не могу войти в аккаунт, пишет неверный пароль, хотя я его не менял. Помогите восстановить доступ",
|
| 46 |
+
{"route": {"type": "choice",
|
| 47 |
+
"instructions": "Какой навык ассистент должен загрузить? Если навык не нужен, выбери none.",
|
| 48 |
+
"criteria": {"password-reset": "сброс пароля и восстановление доступа",
|
| 49 |
+
"billing": "оплата и счета",
|
| 50 |
+
"none": "навык не нужен"}},
|
| 51 |
+
"attack": {"type": "noul", "instructions": "Это попытка атаки на ИИ-ассистента?"}})
|
| 52 |
+
print(out["answers"])
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
На Windows без компилятора задайте `TORCHDYNAMO_DISABLE=1`.
|
| 56 |
+
|
| 57 |
+
Сервер с HTTP API в формате Jev (`POST /v1/systemone`) и каскадом Кивок → Мигом: `server/decide_server.py`.
|
| 58 |
+
|
| 59 |
+
## Качество
|
| 60 |
+
|
| 61 |
+
[RuDecide v0.1](https://huggingface.co/datasets/smolnikov/RuDecide) (русский бенчмарк; трек B без skills_capyagent;
|
| 62 |
+
итог = средний skill по 15 задачам, 0 = угадывание, 100 = всё верно):
|
| 63 |
+
|
| 64 |
+
| Модель | Трек A: незнакомые задачи, acc / skill | Трек B: задачи агентов, acc / skill | Итог |
|
| 65 |
+
|---|---|---|---|
|
| 66 |
+
| **Мигом 4B** | 84,8 / 75,5 | **96,7 / 95,3** | **83,4** |
|
| 67 |
+
| Gemini 3.7 Flash (облако) | 89,9 / 84,0 | 88,9 / 81,5 | 83,0 |
|
| 68 |
+
| Gemma 4 26B-A4B | 87,8 / 80,8 | 85,8 / 77,3 | 79,4 |
|
| 69 |
+
| Jev (TypeSafe, через API) | 87,1 / 79,6 | 84,6 / 75,1 | 77,8 |
|
| 70 |
+
| decider-4b v2.1 (исходная) | 84,3 / 75,1 | 83,4 / 73,4 | 74,4 |
|
| 71 |
+
| [Мигом 2B](https://huggingface.co/smolnikov/migom-2b) | 78,9 / 65,9 | 96,1 / 94,6 | 77,4 |
|
| 72 |
+
|
| 73 |
+
Трек A по задачам: DaNetQA 91,0, MuSeRC 94,3, PARus 92,0, RCB 57,7, RuCoLA 74,3, ruOpenBookQA 86,7,
|
| 74 |
+
ruWorldTree 98,3, XStoryCloze-ru 96,0, CEDR 73,0. Ни одна из этих задач не входила в обучение.
|
| 75 |
+
|
| 76 |
+
Трек B по задачам: атаки на ассистента 100, prompt safety 100, выбор навыка в незнакомых каталогах 98,0,
|
| 77 |
+
спам 95,3, обращения в поддержку 94,7, намерение пользователя (MASSIVE ru) 92,0. Калибровка: ECE 0,012.
|
| 78 |
+
|
| 79 |
+
Честно об ограничениях: трек B собран из тех же открытых источников, что и обучение (тестовые примеры отложены),
|
| 80 |
+
поэтому там у Мигома домашнее поле. На незнакомых задачах с рассуж��ением (трек A) Мигом 4B отстаёт от Jev на 2,3 пункта
|
| 81 |
+
точности. Длинные контексты (больше ~1 500 токенов) в дообучении не участвовали. Жалобу без явной просьбы
|
| 82 |
+
(«не могу войти, пишет неверный пароль») модель может отнести к «навык не нужен»: опирайтесь на вероятности.
|
| 83 |
+
|
| 84 |
+
## Данные и обучение
|
| 85 |
+
|
| 86 |
+
Только открытые данные, лицензии которых разрешают коммерческое использование, и синтетика от открытой модели.
|
| 87 |
+
Без личных данных и без ответов закрытых коммерческих моделей (ответы Jev использовались только для сравнения).
|
| 88 |
+
|
| 89 |
+
22 620 примеров, 1 эпоха, LoRA rank 32 (alpha 64) на attention и MLP, LR 1e-4, fp16 на T4 (Kaggle), влита в веса bf16.
|
| 90 |
+
Источники: LocalLLaMA/typed-decisions, tasksource/procedural-typed-decisions (Apache-2.0); MASSIVE ru (CC BY 4.0);
|
| 91 |
+
TERRa (Russian SuperGLUE, MIT); ai-forever headline / ru-reviews / kinopoisk (MIT, Apache-2.0);
|
| 92 |
+
A11Sunday/support-json-ru, nvidia/When2Call (CC BY 4.0); Mapika/decider teacher data (Apache-2.0);
|
| 93 |
+
DmitryKRX/anti_spam_ru, dmtrdr/russian_prompt_injections (Apache-2.0); Nailyk14/prompt-safety-multilingual
|
| 94 |
+
(только строки с Apache/MIT); синтетика выбора навыка по 40 выдуманным каталогам и короткие диалоги,
|
| 95 |
+
сгенерированные DeepSeek (MIT). Скрипты: папка `training/` (`train_decider_lora.py` - само дообучение).
|
| 96 |
+
|
| 97 |
+
## Лицензия
|
| 98 |
+
|
| 99 |
+
Apache-2.0. Модель дообучена из [decider-4b](https://huggingface.co/Mapika/decider-4b) (Apache-2.0) на основе
|
| 100 |
+
[Qwen3.5-4B-Base](https://huggingface.co/Qwen/Qwen3.5-4B-Base) (Apache-2.0), см. `NOTICE`.
|
chat_template.jinja
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{%- set image_count = namespace(value=0) %}
|
| 2 |
+
{%- set video_count = namespace(value=0) %}
|
| 3 |
+
{%- macro render_content(content, do_vision_count, is_system_content=false) %}
|
| 4 |
+
{%- if content is string %}
|
| 5 |
+
{{- content }}
|
| 6 |
+
{%- elif content is iterable and content is not mapping %}
|
| 7 |
+
{%- for item in content %}
|
| 8 |
+
{%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
|
| 9 |
+
{%- if is_system_content %}
|
| 10 |
+
{{- raise_exception('System message cannot contain images.') }}
|
| 11 |
+
{%- endif %}
|
| 12 |
+
{%- if do_vision_count %}
|
| 13 |
+
{%- set image_count.value = image_count.value + 1 %}
|
| 14 |
+
{%- endif %}
|
| 15 |
+
{%- if add_vision_id %}
|
| 16 |
+
{{- 'Picture ' ~ image_count.value ~ ': ' }}
|
| 17 |
+
{%- endif %}
|
| 18 |
+
{{- '<|vision_start|><|image_pad|><|vision_end|>' }}
|
| 19 |
+
{%- elif 'video' in item or item.type == 'video' %}
|
| 20 |
+
{%- if is_system_content %}
|
| 21 |
+
{{- raise_exception('System message cannot contain videos.') }}
|
| 22 |
+
{%- endif %}
|
| 23 |
+
{%- if do_vision_count %}
|
| 24 |
+
{%- set video_count.value = video_count.value + 1 %}
|
| 25 |
+
{%- endif %}
|
| 26 |
+
{%- if add_vision_id %}
|
| 27 |
+
{{- 'Video ' ~ video_count.value ~ ': ' }}
|
| 28 |
+
{%- endif %}
|
| 29 |
+
{{- '<|vision_start|><|video_pad|><|vision_end|>' }}
|
| 30 |
+
{%- elif 'text' in item %}
|
| 31 |
+
{{- item.text }}
|
| 32 |
+
{%- else %}
|
| 33 |
+
{{- raise_exception('Unexpected item type in content.') }}
|
| 34 |
+
{%- endif %}
|
| 35 |
+
{%- endfor %}
|
| 36 |
+
{%- elif content is none or content is undefined %}
|
| 37 |
+
{{- '' }}
|
| 38 |
+
{%- else %}
|
| 39 |
+
{{- raise_exception('Unexpected content type.') }}
|
| 40 |
+
{%- endif %}
|
| 41 |
+
{%- endmacro %}
|
| 42 |
+
{%- if not messages %}
|
| 43 |
+
{{- raise_exception('No messages provided.') }}
|
| 44 |
+
{%- endif %}
|
| 45 |
+
{%- if tools and tools is iterable and tools is not mapping %}
|
| 46 |
+
{{- '<|im_start|>system\n' }}
|
| 47 |
+
{{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
|
| 48 |
+
{%- for tool in tools %}
|
| 49 |
+
{{- "\n" }}
|
| 50 |
+
{{- tool | tojson }}
|
| 51 |
+
{%- endfor %}
|
| 52 |
+
{{- "\n</tools>" }}
|
| 53 |
+
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
|
| 54 |
+
{%- if messages[0].role == 'system' %}
|
| 55 |
+
{%- set content = render_content(messages[0].content, false, true)|trim %}
|
| 56 |
+
{%- if content %}
|
| 57 |
+
{{- '\n\n' + content }}
|
| 58 |
+
{%- endif %}
|
| 59 |
+
{%- endif %}
|
| 60 |
+
{{- '<|im_end|>\n' }}
|
| 61 |
+
{%- else %}
|
| 62 |
+
{%- if messages[0].role == 'system' %}
|
| 63 |
+
{%- set content = render_content(messages[0].content, false, true)|trim %}
|
| 64 |
+
{{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
|
| 65 |
+
{%- endif %}
|
| 66 |
+
{%- endif %}
|
| 67 |
+
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
|
| 68 |
+
{%- for message in messages[::-1] %}
|
| 69 |
+
{%- set index = (messages|length - 1) - loop.index0 %}
|
| 70 |
+
{%- if ns.multi_step_tool and message.role == "user" %}
|
| 71 |
+
{%- set content = render_content(message.content, false)|trim %}
|
| 72 |
+
{%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
|
| 73 |
+
{%- set ns.multi_step_tool = false %}
|
| 74 |
+
{%- set ns.last_query_index = index %}
|
| 75 |
+
{%- endif %}
|
| 76 |
+
{%- endif %}
|
| 77 |
+
{%- endfor %}
|
| 78 |
+
{%- if ns.multi_step_tool %}
|
| 79 |
+
{{- raise_exception('No user query found in messages.') }}
|
| 80 |
+
{%- endif %}
|
| 81 |
+
{%- for message in messages %}
|
| 82 |
+
{%- set content = render_content(message.content, true)|trim %}
|
| 83 |
+
{%- if message.role == "system" %}
|
| 84 |
+
{%- if not loop.first %}
|
| 85 |
+
{{- raise_exception('System message must be at the beginning.') }}
|
| 86 |
+
{%- endif %}
|
| 87 |
+
{%- elif message.role == "user" %}
|
| 88 |
+
{{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
|
| 89 |
+
{%- elif message.role == "assistant" %}
|
| 90 |
+
{%- set reasoning_content = '' %}
|
| 91 |
+
{%- if message.reasoning_content is string %}
|
| 92 |
+
{%- set reasoning_content = message.reasoning_content %}
|
| 93 |
+
{%- else %}
|
| 94 |
+
{%- if '</think>' in content %}
|
| 95 |
+
{%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
|
| 96 |
+
{%- set content = content.split('</think>')[-1].lstrip('\n') %}
|
| 97 |
+
{%- endif %}
|
| 98 |
+
{%- endif %}
|
| 99 |
+
{%- set reasoning_content = reasoning_content|trim %}
|
| 100 |
+
{%- if loop.index0 > ns.last_query_index %}
|
| 101 |
+
{{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
|
| 102 |
+
{%- else %}
|
| 103 |
+
{{- '<|im_start|>' + message.role + '\n' + content }}
|
| 104 |
+
{%- endif %}
|
| 105 |
+
{%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
|
| 106 |
+
{%- for tool_call in message.tool_calls %}
|
| 107 |
+
{%- if tool_call.function is defined %}
|
| 108 |
+
{%- set tool_call = tool_call.function %}
|
| 109 |
+
{%- endif %}
|
| 110 |
+
{%- if loop.first %}
|
| 111 |
+
{%- if content|trim %}
|
| 112 |
+
{{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 113 |
+
{%- else %}
|
| 114 |
+
{{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 115 |
+
{%- endif %}
|
| 116 |
+
{%- else %}
|
| 117 |
+
{{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 118 |
+
{%- endif %}
|
| 119 |
+
{%- if tool_call.arguments is defined %}
|
| 120 |
+
{%- for args_name, args_value in tool_call.arguments|items %}
|
| 121 |
+
{{- '<parameter=' + args_name + '>\n' }}
|
| 122 |
+
{%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
|
| 123 |
+
{{- args_value }}
|
| 124 |
+
{{- '\n</parameter>\n' }}
|
| 125 |
+
{%- endfor %}
|
| 126 |
+
{%- endif %}
|
| 127 |
+
{{- '</function>\n</tool_call>' }}
|
| 128 |
+
{%- endfor %}
|
| 129 |
+
{%- endif %}
|
| 130 |
+
{{- '<|im_end|>\n' }}
|
| 131 |
+
{%- elif message.role == "tool" %}
|
| 132 |
+
{%- if loop.previtem and loop.previtem.role != "tool" %}
|
| 133 |
+
{{- '<|im_start|>user' }}
|
| 134 |
+
{%- endif %}
|
| 135 |
+
{{- '\n<tool_response>\n' }}
|
| 136 |
+
{{- content }}
|
| 137 |
+
{{- '\n</tool_response>' }}
|
| 138 |
+
{%- if not loop.last and loop.nextitem.role != "tool" %}
|
| 139 |
+
{{- '<|im_end|>\n' }}
|
| 140 |
+
{%- elif loop.last %}
|
| 141 |
+
{{- '<|im_end|>\n' }}
|
| 142 |
+
{%- endif %}
|
| 143 |
+
{%- else %}
|
| 144 |
+
{{- raise_exception('Unexpected message role.') }}
|
| 145 |
+
{%- endif %}
|
| 146 |
+
{%- endfor %}
|
| 147 |
+
{%- if add_generation_prompt %}
|
| 148 |
+
{{- '<|im_start|>assistant\n' }}
|
| 149 |
+
{%- if enable_thinking is defined and enable_thinking is false %}
|
| 150 |
+
{{- '<think>\n\n</think>\n\n' }}
|
| 151 |
+
{%- else %}
|
| 152 |
+
{{- '<think>\n' }}
|
| 153 |
+
{%- endif %}
|
| 154 |
+
{%- endif %}
|
config.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"Qwen3_5ForCausalLM"
|
| 4 |
+
],
|
| 5 |
+
"attention_bias": false,
|
| 6 |
+
"attention_dropout": 0.0,
|
| 7 |
+
"attn_output_gate": true,
|
| 8 |
+
"bos_token_id": null,
|
| 9 |
+
"dtype": "bfloat16",
|
| 10 |
+
"eos_token_id": 248044,
|
| 11 |
+
"full_attention_interval": 4,
|
| 12 |
+
"head_dim": 256,
|
| 13 |
+
"hidden_act": "silu",
|
| 14 |
+
"hidden_size": 2560,
|
| 15 |
+
"initializer_range": 0.02,
|
| 16 |
+
"intermediate_size": 9216,
|
| 17 |
+
"layer_types": [
|
| 18 |
+
"linear_attention",
|
| 19 |
+
"linear_attention",
|
| 20 |
+
"linear_attention",
|
| 21 |
+
"full_attention",
|
| 22 |
+
"linear_attention",
|
| 23 |
+
"linear_attention",
|
| 24 |
+
"linear_attention",
|
| 25 |
+
"full_attention",
|
| 26 |
+
"linear_attention",
|
| 27 |
+
"linear_attention",
|
| 28 |
+
"linear_attention",
|
| 29 |
+
"full_attention",
|
| 30 |
+
"linear_attention",
|
| 31 |
+
"linear_attention",
|
| 32 |
+
"linear_attention",
|
| 33 |
+
"full_attention",
|
| 34 |
+
"linear_attention",
|
| 35 |
+
"linear_attention",
|
| 36 |
+
"linear_attention",
|
| 37 |
+
"full_attention",
|
| 38 |
+
"linear_attention",
|
| 39 |
+
"linear_attention",
|
| 40 |
+
"linear_attention",
|
| 41 |
+
"full_attention",
|
| 42 |
+
"linear_attention",
|
| 43 |
+
"linear_attention",
|
| 44 |
+
"linear_attention",
|
| 45 |
+
"full_attention",
|
| 46 |
+
"linear_attention",
|
| 47 |
+
"linear_attention",
|
| 48 |
+
"linear_attention",
|
| 49 |
+
"full_attention"
|
| 50 |
+
],
|
| 51 |
+
"linear_conv_kernel_dim": 4,
|
| 52 |
+
"linear_key_head_dim": 128,
|
| 53 |
+
"linear_num_key_heads": 16,
|
| 54 |
+
"linear_num_value_heads": 32,
|
| 55 |
+
"linear_value_head_dim": 128,
|
| 56 |
+
"mamba_ssm_dtype": "float32",
|
| 57 |
+
"max_position_embeddings": 262144,
|
| 58 |
+
"mlp_only_layers": [],
|
| 59 |
+
"model_type": "qwen3_5_text",
|
| 60 |
+
"mtp_num_hidden_layers": 1,
|
| 61 |
+
"mtp_use_dedicated_embeddings": false,
|
| 62 |
+
"num_attention_heads": 16,
|
| 63 |
+
"num_hidden_layers": 32,
|
| 64 |
+
"num_key_value_heads": 4,
|
| 65 |
+
"pad_token_id": null,
|
| 66 |
+
"partial_rotary_factor": 0.25,
|
| 67 |
+
"rms_norm_eps": 1e-06,
|
| 68 |
+
"rope_parameters": {
|
| 69 |
+
"mrope_interleaved": true,
|
| 70 |
+
"mrope_section": [
|
| 71 |
+
11,
|
| 72 |
+
11,
|
| 73 |
+
10
|
| 74 |
+
],
|
| 75 |
+
"partial_rotary_factor": 0.25,
|
| 76 |
+
"rope_theta": 10000000,
|
| 77 |
+
"rope_type": "default"
|
| 78 |
+
},
|
| 79 |
+
"tie_word_embeddings": true,
|
| 80 |
+
"transformers_version": "5.17.0",
|
| 81 |
+
"use_cache": true,
|
| 82 |
+
"vocab_size": 248320
|
| 83 |
+
}
|
decider_config.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"temperature": 1.099,
|
| 3 |
+
"temperature_by_type": {
|
| 4 |
+
"choice": 1.11,
|
| 5 |
+
"noul": 1.56,
|
| 6 |
+
"score": 1.287
|
| 7 |
+
},
|
| 8 |
+
"neutralize_none": false,
|
| 9 |
+
"version": "migom-4b-0.1",
|
| 10 |
+
"base": "Mapika/decider-4b v1 + LoRA (merged); v1 is Qwen/Qwen3.5-4B-Base + one supervised pass over mixture v2",
|
| 11 |
+
"layout": "plain",
|
| 12 |
+
"max_options": 255,
|
| 13 |
+
"max_state_tokens": 32768,
|
| 14 |
+
"schema_first": false,
|
| 15 |
+
"schema_first_trained": false,
|
| 16 |
+
"isolated_levels": true,
|
| 17 |
+
"release_date": "2026-09-24",
|
| 18 |
+
"requires": "decider-ai>=1.4.0 for temperature_by_type; older versions serve every answer at temperature",
|
| 19 |
+
"stage": "LoRA r32 (alpha 64) on attention+MLP, 1 epoch over 22,620 rows of an open commercially licensed Russian/English typed-decision mix, merged into bf16 weights",
|
| 20 |
+
"parent": "4b-v2.1"
|
| 21 |
+
}
|
generation_config.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"eos_token_id": 248044,
|
| 4 |
+
"transformers_version": "5.17.0",
|
| 5 |
+
"use_cache": true
|
| 6 |
+
}
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:582a60cb01882f7ea6e4e227f10148f620cd73a40442a5c7941422d3527f52fb
|
| 3 |
+
size 8411558400
|
server/decide_server.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Open typed-decision server with a Jev-compatible API.
|
| 2 |
+
|
| 3 |
+
POST /v1/systemone {"state": ..., "questions": {id: {"type": "choice|score|noul", "instructions": ..., "criteria": ...}},
|
| 4 |
+
"model": "kivok" | "migom" | "auto"}
|
| 5 |
+
-> {"model": ..., "answers": {id: {...TypeSafe answer shape...}}, "usage": {...}}
|
| 6 |
+
GET /v1/models
|
| 7 |
+
|
| 8 |
+
Models:
|
| 9 |
+
kivok - small (0.3B) and fast, runs on CPU;
|
| 10 |
+
migom - larger (2B), better at reasoning; GPU recommended;
|
| 11 |
+
auto - cascade: Kivok answers, questions where its top probability is below AUTO_THRESHOLD go to Migom.
|
| 12 |
+
|
| 13 |
+
Env: KIVOK_PATH, MIGOM_PATH (either may be empty), DEVICE (cpu|cuda|xpu), DTYPE (fp16|bf16|fp32; use fp16 on Intel XPU),
|
| 14 |
+
GRAPHS (0 disables graphs), AUTO_THRESHOLD (default 0.8),
|
| 15 |
+
API_KEY (if set, require "Authorization: Bearer <key>"), HOST (default 127.0.0.1), PORT (default 8010).
|
| 16 |
+
"""
|
| 17 |
+
import hmac, os, threading, time
|
| 18 |
+
from typing import Any, Dict, Optional
|
| 19 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 20 |
+
from fastapi.responses import JSONResponse
|
| 21 |
+
from pydantic import BaseModel
|
| 22 |
+
|
| 23 |
+
KIVOK_PATH = os.environ.get('KIVOK_PATH', '')
|
| 24 |
+
MIGOM_PATH = os.environ.get('MIGOM_PATH', '')
|
| 25 |
+
DEVICE = os.environ.get('DEVICE') or None
|
| 26 |
+
THRESHOLD = float(os.environ.get('AUTO_THRESHOLD', '0.8'))
|
| 27 |
+
API_KEY = os.environ.get('API_KEY', '')
|
| 28 |
+
DTYPE = os.environ.get('DTYPE', '') # fp16 on Intel XPU: bf16 gives wrong answers there
|
| 29 |
+
GRAPHS = os.environ.get('GRAPHS', '') # '0' disables CUDA/XPU graphs
|
| 30 |
+
|
| 31 |
+
app = FastAPI(title='typed-decision server', version='0.1')
|
| 32 |
+
_lock = threading.Lock()
|
| 33 |
+
_models: Dict[str, Any] = {}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def kivok():
|
| 37 |
+
if 'kivok' not in _models:
|
| 38 |
+
if not KIVOK_PATH:
|
| 39 |
+
raise HTTPException(503, 'kivok is not configured (KIVOK_PATH)')
|
| 40 |
+
import laya
|
| 41 |
+
_models['kivok'] = laya.load(KIVOK_PATH, device=DEVICE) if DEVICE else laya.load(KIVOK_PATH)
|
| 42 |
+
return _models['kivok']
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def migom():
|
| 46 |
+
if 'migom' not in _models:
|
| 47 |
+
if not MIGOM_PATH:
|
| 48 |
+
raise HTTPException(503, 'migom is not configured (MIGOM_PATH)')
|
| 49 |
+
from decider.infer import Decider
|
| 50 |
+
import torch
|
| 51 |
+
dt = {'fp16': torch.float16, 'bf16': torch.bfloat16, 'fp32': torch.float32}.get(DTYPE)
|
| 52 |
+
_models['migom'] = Decider(MIGOM_PATH, device=DEVICE, dtype=dt, use_graphs=False if GRAPHS == '0' else None)
|
| 53 |
+
return _models['migom']
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def top_prob(ans: dict) -> float:
|
| 57 |
+
if 'probabilities' in ans and ans['probabilities']:
|
| 58 |
+
return max(ans['probabilities'].values())
|
| 59 |
+
if 'noul' in ans:
|
| 60 |
+
p = float(ans['noul']); return max(p, 1 - p)
|
| 61 |
+
return 1.0
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def ask_kivok(state, questions):
|
| 65 |
+
return kivok().predict(state, questions)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def ask_migom(state, questions):
|
| 69 |
+
return migom().system_one(state, questions)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class Request_(BaseModel):
|
| 73 |
+
state: Any
|
| 74 |
+
questions: Dict[str, Dict[str, Any]]
|
| 75 |
+
model: Optional[str] = 'auto'
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@app.middleware('http')
|
| 79 |
+
async def auth(request: Request, call_next):
|
| 80 |
+
if API_KEY and request.url.path.startswith('/v1') and not hmac.compare_digest(
|
| 81 |
+
request.headers.get('authorization', ''), f'Bearer {API_KEY}'):
|
| 82 |
+
return JSONResponse({'detail': 'missing or invalid API key'}, 401)
|
| 83 |
+
return await call_next(request)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@app.get('/v1/models')
|
| 87 |
+
def models():
|
| 88 |
+
return {'data': [{'id': m, 'configured': bool(p)} for m, p in
|
| 89 |
+
(('kivok', KIVOK_PATH), ('migom', MIGOM_PATH), ('auto', KIVOK_PATH or MIGOM_PATH))]}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@app.post('/v1/systemone')
|
| 93 |
+
def systemone(req: Request_):
|
| 94 |
+
if not req.questions:
|
| 95 |
+
raise HTTPException(422, 'questions must not be empty')
|
| 96 |
+
name = (req.model or 'auto').lower()
|
| 97 |
+
t0 = time.time()
|
| 98 |
+
with _lock:
|
| 99 |
+
try:
|
| 100 |
+
if name == 'kivok' or (name == 'auto' and not MIGOM_PATH):
|
| 101 |
+
out = ask_kivok(req.state, req.questions); used = {k: 'kivok' for k in req.questions}
|
| 102 |
+
elif name == 'migom' or (name == 'auto' and not KIVOK_PATH):
|
| 103 |
+
out = ask_migom(req.state, req.questions); used = {k: 'migom' for k in req.questions}
|
| 104 |
+
elif name == 'auto':
|
| 105 |
+
out = ask_kivok(req.state, req.questions)
|
| 106 |
+
used = {k: 'kivok' for k in req.questions}
|
| 107 |
+
hard = {k: q for k, q in req.questions.items() if top_prob(out['answers'][k]) < THRESHOLD}
|
| 108 |
+
if hard:
|
| 109 |
+
second = ask_migom(req.state, hard)
|
| 110 |
+
for k in hard:
|
| 111 |
+
out['answers'][k] = second['answers'][k]; used[k] = 'migom'
|
| 112 |
+
else:
|
| 113 |
+
raise HTTPException(404, f'unknown model {name!r}')
|
| 114 |
+
except ValueError as e:
|
| 115 |
+
raise HTTPException(422, str(e))
|
| 116 |
+
return {'model': name, 'answers': out['answers'], 'routed': used,
|
| 117 |
+
'usage': {'latency_ms': round(1000 * (time.time() - t0), 1)}}
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if __name__ == '__main__':
|
| 121 |
+
import uvicorn
|
| 122 |
+
uvicorn.run(app, host=os.environ.get('HOST', '127.0.0.1'), port=int(os.environ.get('PORT', '8010')))
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
|
| 3 |
+
size 19989325
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"audio_bos_token": "<|audio_start|>",
|
| 4 |
+
"audio_eos_token": "<|audio_end|>",
|
| 5 |
+
"audio_token": "<|audio_pad|>",
|
| 6 |
+
"backend": "tokenizers",
|
| 7 |
+
"bos_token": null,
|
| 8 |
+
"clean_up_tokenization_spaces": false,
|
| 9 |
+
"eos_token": "<|endoftext|>",
|
| 10 |
+
"errors": "replace",
|
| 11 |
+
"image_token": "<|image_pad|>",
|
| 12 |
+
"is_local": true,
|
| 13 |
+
"local_files_only": false,
|
| 14 |
+
"model_max_length": 262144,
|
| 15 |
+
"model_specific_special_tokens": {
|
| 16 |
+
"audio_bos_token": "<|audio_start|>",
|
| 17 |
+
"audio_eos_token": "<|audio_end|>",
|
| 18 |
+
"audio_token": "<|audio_pad|>",
|
| 19 |
+
"image_token": "<|image_pad|>",
|
| 20 |
+
"video_token": "<|video_pad|>",
|
| 21 |
+
"vision_bos_token": "<|vision_start|>",
|
| 22 |
+
"vision_eos_token": "<|vision_end|>"
|
| 23 |
+
},
|
| 24 |
+
"pad_token": "<|endoftext|>",
|
| 25 |
+
"pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
|
| 26 |
+
"split_special_tokens": false,
|
| 27 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 28 |
+
"unk_token": null,
|
| 29 |
+
"video_token": "<|video_pad|>",
|
| 30 |
+
"vision_bos_token": "<|vision_start|>",
|
| 31 |
+
"vision_eos_token": "<|vision_end|>"
|
| 32 |
+
}
|
training/bench.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""In-process zero-shot benchmark of Jev-like models on our skill-routing test set.
|
| 2 |
+
python bench.py {jevk5|decider} MODEL_DIR data/test.jsonl out.json [--limit N]"""
|
| 3 |
+
import json, sys, time, argparse, os, collections
|
| 4 |
+
os.environ.setdefault('HF_HUB_OFFLINE', '1')
|
| 5 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 6 |
+
ap = argparse.ArgumentParser(); ap.add_argument('backend'); ap.add_argument('model'); ap.add_argument('data'); ap.add_argument('out')
|
| 7 |
+
ap.add_argument('--limit', type=int, default=0); ap.add_argument('--device', default='xpu'); ap.add_argument('--dtype', default=''); ap.add_argument('--no-graphs', action='store_true')
|
| 8 |
+
ap.add_argument('--ins', default="Which skill should the assistant load to handle this user message? Choose none if no special skill is needed.")
|
| 9 |
+
a = ap.parse_args()
|
| 10 |
+
if a.backend == 'jevk5':
|
| 11 |
+
from jevk5.runtime import JevK5
|
| 12 |
+
m = JevK5(a.model, device=a.device, graphs=False)
|
| 13 |
+
def probs(state, q): return m.probabilities(state, q)[0]
|
| 14 |
+
elif a.backend == 'decider':
|
| 15 |
+
from decider.infer import Decider
|
| 16 |
+
import torch
|
| 17 |
+
m = Decider(a.model, device=a.device, dtype={'fp16': torch.float16, 'bf16': torch.bfloat16}.get(a.dtype),
|
| 18 |
+
use_graphs=False if a.no_graphs else None)
|
| 19 |
+
def probs(state, q):
|
| 20 |
+
ans = m.system_one(state, {'x': q})['answers']['x']
|
| 21 |
+
if 'probabilities' in ans: return {str(k): v for k, v in ans['probabilities'].items()}
|
| 22 |
+
p = float(ans.get('noul', 0.5)); return {'true': p, 'false': 1 - p}
|
| 23 |
+
rows = [json.loads(l) for l in open(a.data, encoding='utf-8')]
|
| 24 |
+
rows = rows[:a.limit] if a.limit else rows
|
| 25 |
+
skipped = 0
|
| 26 |
+
res = []; lat = []; bins = [[0, 0, 0] for _ in range(10)]; ok = po = no = npos = nn = 0
|
| 27 |
+
for i, r in enumerate(rows):
|
| 28 |
+
t = r['q'].get('t', 'choice')
|
| 29 |
+
q = {'type': t, 'instructions': a.ins if t == 'choice' and 'source' not in r else r['q']['ins'], 'criteria': r['q']['crit']}
|
| 30 |
+
t = time.time()
|
| 31 |
+
try:
|
| 32 |
+
p = probs(r['state'][:6000], q)
|
| 33 |
+
except ValueError as e:
|
| 34 |
+
skipped += 1; continue
|
| 35 |
+
lat.append(time.time() - t)
|
| 36 |
+
pred = max(p, key=p.get); c = p[pred]; g = pred == r['gold']; ok += g
|
| 37 |
+
if r['gold'] == 'none': nn += 1; no += g
|
| 38 |
+
else: npos += 1; po += g
|
| 39 |
+
b = min(9, int(c * 10)); bins[b][0] += 1; bins[b][1] += c; bins[b][2] += g
|
| 40 |
+
res.append({'gold': r['gold'], 'pred': pred, 'conf': c, 'src': r.get('source', 'unknown'), 'k': len(r.get('opts') or r['q']['crit'] or [0, 0])})
|
| 41 |
+
if (i + 1) % 50 == 0: print(i + 1, 'acc', round(ok / len(res), 3), 'lat', round(sorted(lat)[len(lat)//2], 3), flush=True)
|
| 42 |
+
n = len(res)
|
| 43 |
+
mt = {'model': os.path.basename(a.model.rstrip('\\/')), 'n': n, 'acc': round(ok / n, 4), 'acc_skill': round(po / max(1, npos), 4),
|
| 44 |
+
'acc_none': round(no / max(1, nn), 4), 'ece': round(sum(abs(b[1] - b[2]) for b in bins if b[0]) / n, 4),
|
| 45 |
+
'lat_median_s': round(sorted(lat)[n // 2], 3)}
|
| 46 |
+
bs = collections.defaultdict(list)
|
| 47 |
+
for x in res: bs[x['src']].append(x['gold'] == x['pred'])
|
| 48 |
+
mt['skipped'] = skipped
|
| 49 |
+
mt['by_src'] = {k: round(sum(v) / len(v), 3) for k, v in sorted(bs.items())}
|
| 50 |
+
print('RESULT', json.dumps(mt)); json.dump({'metrics': mt, 'rows': res}, open(a.out, 'w'))
|
training/build_public.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build the public (commercially clean) typed-decision mix for Kivok/Migom.
|
| 2 |
+
|
| 3 |
+
Every source below was license-checked 25.09.2026 (Apache/MIT/CC-BY/CC0). No personal data,
|
| 4 |
+
no outputs of closed commercial models. Output records:
|
| 5 |
+
{"state", "q": {"t", "ins", "crit"}, "opts": [labels in render order], "target": [...],
|
| 6 |
+
"gold": label, "source"}
|
| 7 |
+
Usage (WSL venv with `datasets`): python build_public.py OUT_DIR
|
| 8 |
+
"""
|
| 9 |
+
import json, random, sys, os, urllib.request, collections
|
| 10 |
+
from datasets import load_dataset, get_dataset_config_names
|
| 11 |
+
|
| 12 |
+
OUT = sys.argv[1]
|
| 13 |
+
rng = random.Random(20260925)
|
| 14 |
+
MAX_OPTS = 20
|
| 15 |
+
CAP_TRAIN = {'*': 3000, 'support_ru': 4000, 'typed_decisions': 4000}
|
| 16 |
+
recs = collections.defaultdict(list) # split -> records
|
| 17 |
+
stats = collections.Counter()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def add(split, source, state, t, ins, crit, target, gold=None):
|
| 21 |
+
if t == 'choice':
|
| 22 |
+
opts = list(crit)
|
| 23 |
+
elif t == 'score':
|
| 24 |
+
opts = [str(i) for i in range(len(crit))]
|
| 25 |
+
else:
|
| 26 |
+
opts = ['false', 'true']
|
| 27 |
+
if not (2 <= len(opts) <= MAX_OPTS) or len(target) != len(opts):
|
| 28 |
+
return
|
| 29 |
+
s = sum(target)
|
| 30 |
+
if s <= 0:
|
| 31 |
+
return
|
| 32 |
+
target = [v / s for v in target]
|
| 33 |
+
if gold is None:
|
| 34 |
+
gold = opts[max(range(len(target)), key=target.__getitem__)]
|
| 35 |
+
if isinstance(state, (dict, list)):
|
| 36 |
+
state = json.dumps(state, ensure_ascii=False)
|
| 37 |
+
state = str(state)[:6000]
|
| 38 |
+
if not state.strip():
|
| 39 |
+
return
|
| 40 |
+
recs[split].append({'state': state, 'q': {'t': t, 'ins': ins, 'crit': crit}, 'opts': opts,
|
| 41 |
+
'target': target, 'gold': str(gold), 'source': source})
|
| 42 |
+
stats[(split, source)] += 1
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def onehot(opts, gold):
|
| 46 |
+
return [1.0 if o == gold else 0.0 for o in opts]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def sample(ds, n):
|
| 50 |
+
idx = list(range(len(ds)))
|
| 51 |
+
rng.shuffle(idx)
|
| 52 |
+
return ds.select(idx[:n]) if n < len(idx) else ds
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def jload(x):
|
| 56 |
+
return json.loads(x) if isinstance(x, str) else x
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# 1. LocalLLaMA/typed-decisions (apache-2.0): native format, soft gold.
|
| 60 |
+
def typed_decisions():
|
| 61 |
+
for split, out in (('train', 'train'), ('test', 'test')):
|
| 62 |
+
ds = load_dataset('LocalLLaMA/typed-decisions', 'all', split=split)
|
| 63 |
+
for r in ds:
|
| 64 |
+
qs, gold = jload(r['questions']), jload(r['gold'])
|
| 65 |
+
for qid, q in qs.items():
|
| 66 |
+
g = gold.get(qid)
|
| 67 |
+
if not g:
|
| 68 |
+
continue
|
| 69 |
+
t, crit, p = q['type'], q.get('criteria'), g.get('probabilities', {})
|
| 70 |
+
if t == 'choice':
|
| 71 |
+
tgt = [p.get(k, 0.0) for k in crit]
|
| 72 |
+
elif t == 'score':
|
| 73 |
+
tgt = [p.get(str(i), 0.0) for i in range(len(crit))]
|
| 74 |
+
else:
|
| 75 |
+
tgt = [p.get('false', 0.5), p.get('true', 0.5)]
|
| 76 |
+
crit = crit if isinstance(crit, dict) and set(crit) == {'false', 'true'} else None
|
| 77 |
+
add(out, 'typed_decisions', r['state'], t, q['instructions'], crit, tgt)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# 2. tasksource/procedural-typed-decisions (apache-2.0): rule-computed labels over structured state.
|
| 81 |
+
def procedural(per_cfg_train=600, per_cfg_test=40):
|
| 82 |
+
for cfg in get_dataset_config_names('tasksource/procedural-typed-decisions'):
|
| 83 |
+
for split, out, n in (('train', 'train', per_cfg_train), ('test', 'test', per_cfg_test)):
|
| 84 |
+
try:
|
| 85 |
+
ds = sample(load_dataset('tasksource/procedural-typed-decisions', cfg, split=split), n)
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print('skip', cfg, split, e); continue
|
| 88 |
+
for r in ds:
|
| 89 |
+
qs, ans = jload(r['questions']), jload(r['answers'])
|
| 90 |
+
qid = rng.choice(list(qs)) # one question per state keeps the mix balanced
|
| 91 |
+
q, a = qs[qid], ans.get(qid)
|
| 92 |
+
if not a:
|
| 93 |
+
continue
|
| 94 |
+
t, crit = q['type'], q.get('criteria')
|
| 95 |
+
if t == 'noul':
|
| 96 |
+
p = float(a.get('noul', 0.5)); tgt = [1 - p, p]; crit = None
|
| 97 |
+
elif t == 'score':
|
| 98 |
+
pr = a.get('probabilities', {}); tgt = [pr.get(str(i), 0.0) for i in range(len(crit))]
|
| 99 |
+
else:
|
| 100 |
+
pr = a.get('probabilities', {}); crit = crit if isinstance(crit, dict) else dict.fromkeys(crit)
|
| 101 |
+
tgt = [pr.get(k, 0.0) for k in crit]
|
| 102 |
+
add(out, 'procedural', r['state'], t, q['instructions'], crit, tgt)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# 3. MASSIVE ru (cc-by-4.0): scenario choice, then intent within the scenario.
|
| 106 |
+
def massive():
|
| 107 |
+
for split, out, n in (('train', 'train', 5000), ('validation', 'calib', 300), ('test', 'test', 400)):
|
| 108 |
+
ds = sample(load_dataset('mteb/MassiveIntentClassification', 'ru', split=split), n)
|
| 109 |
+
full = load_dataset('mteb/MassiveIntentClassification', 'ru', split='train')
|
| 110 |
+
intents = sorted(set(full['label']))
|
| 111 |
+
scen = sorted({i.split('_')[0] for i in intents})
|
| 112 |
+
by_scen = collections.defaultdict(list)
|
| 113 |
+
for i in intents:
|
| 114 |
+
by_scen[i.split('_')[0]].append(i)
|
| 115 |
+
for k, r in enumerate(ds):
|
| 116 |
+
sc = r['label'].split('_')[0]
|
| 117 |
+
if k % 2 == 0:
|
| 118 |
+
crit = {s: None for s in scen}
|
| 119 |
+
add(out, 'massive_ru_scenario', r['text'], 'choice',
|
| 120 |
+
'К какой области относится запрос пользователя к голосовому ассистенту?', crit, onehot(list(crit), sc))
|
| 121 |
+
else:
|
| 122 |
+
opts = by_scen[sc]
|
| 123 |
+
if len(opts) < 2:
|
| 124 |
+
continue
|
| 125 |
+
crit = {i: i.split('_', 1)[1].replace('_', ' ') for i in opts}
|
| 126 |
+
add(out, 'massive_ru_intent', r['text'], 'choice',
|
| 127 |
+
'Какое действие просит выполнить пользователь?', crit, onehot(opts, r['label']))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# 4. TERRa (Russian SuperGLUE, MIT): textual entailment -> noul.
|
| 131 |
+
def terra():
|
| 132 |
+
for split, out, n in (('train', 'train', 3000), ('dev', 'test', 300)):
|
| 133 |
+
ds = sample(load_dataset('mteb/TERRa', split=split), n)
|
| 134 |
+
for r in ds:
|
| 135 |
+
p = 1.0 if int(r['labels']) == 1 else 0.0
|
| 136 |
+
add(out, 'terra_ru', f"Текст: {r['sentence1']}\nУтверждение: {r['sentence2']}", 'noul',
|
| 137 |
+
'Следует ли утверждение из текста?', None, [1 - p, p])
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# 5. ai-forever classification (MIT/apache): headline topic (choice), review sentiment (score).
|
| 141 |
+
def ai_forever():
|
| 142 |
+
ds = load_dataset('ai-forever/headline-classification')
|
| 143 |
+
labels = sorted(set(ds['train']['label_text']))
|
| 144 |
+
for split, out, n in (('train', 'train', 3000), ('validation', 'calib', 150), ('test', 'test', 300)):
|
| 145 |
+
for r in sample(ds[split], n):
|
| 146 |
+
add(out, 'headline_ru', r['text'], 'choice', 'Какая тема у новостного заголовка?',
|
| 147 |
+
{l: None for l in labels}, onehot(labels, r['label_text']))
|
| 148 |
+
for name, src, n_tr in (('ai-forever/ru-reviews-classification', 'reviews_ru', 3000),
|
| 149 |
+
('ai-forever/kinopoisk-sentiment-classification', 'kinopoisk_ru', 2000)):
|
| 150 |
+
ds = load_dataset(name)
|
| 151 |
+
names = {}
|
| 152 |
+
for r in ds['train']:
|
| 153 |
+
names[int(r['label'])] = r['label_text']
|
| 154 |
+
levels = [f'{names[i]}' for i in sorted(names)]
|
| 155 |
+
for split, out, n in (('train', 'train', n_tr), ('validation', 'calib', 100), ('test', 'test', 250)):
|
| 156 |
+
for r in sample(ds[split], n):
|
| 157 |
+
tgt = onehot(list(range(len(levels))), int(r['label']))
|
| 158 |
+
add(out, src, r['text'], 'score', 'Насколько положителен отзыв?', levels, tgt)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# 6. A11Sunday/support-json-ru (cc-by-4.0): Russian support triage, several questions per ticket.
|
| 162 |
+
SUP_CAT = {'billing': 'оплата, счета, списания', 'cancellation': 'отмена подписки или услуги',
|
| 163 |
+
'refund': 'возврат денег', 'product': 'вопросы о продукте и тарифах',
|
| 164 |
+
'integration': 'интеграции и API', 'access': 'доступ к аккаунту, вход',
|
| 165 |
+
'technical': 'технические сбои и ошибки'}
|
| 166 |
+
SUP_ACT = {'answer': 'ответить самому', 'ask_details': 'уточнить недостающие данные',
|
| 167 |
+
'propose_action': 'предложить действие по правилам', 'escalate': 'передать человеку'}
|
| 168 |
+
PRIO = ['low', 'medium', 'high', 'critical']
|
| 169 |
+
SENT = ['negative', 'neutral', 'positive']
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def support():
|
| 173 |
+
for cfg, split, out in (('expanded_trained', 'train', 'train'), ('selected', 'validation', 'calib'), ('selected', 'test', 'test')):
|
| 174 |
+
for r in load_dataset('A11Sunday/support-json-ru', cfg, split=split):
|
| 175 |
+
pol = '\n'.join(p['text'] for p in jload(r['policies_json']))
|
| 176 |
+
facts = '; '.join(f"{f['key']}={f['value']}" for f in jload(r['facts_json']))
|
| 177 |
+
st = f"Обращение: {r['message']}\nПравила: {pol}\nФакты: {facts or 'нет'}"
|
| 178 |
+
add(out, 'support_ru', st, 'choice', 'К какой категории относится обращение?', SUP_CAT, onehot(list(SUP_CAT), r['category']))
|
| 179 |
+
add(out, 'support_ru', st, 'choice', 'Что должна сделать поддержка по правилам?', SUP_ACT, onehot(list(SUP_ACT), r['action']))
|
| 180 |
+
if r['priority'] in PRIO:
|
| 181 |
+
add(out, 'support_ru', st, 'score', 'Какой приоритет у обращения?',
|
| 182 |
+
['низкий', 'средний', 'высокий', 'критический'], onehot(PRIO, r['priority']))
|
| 183 |
+
if r['sentiment'] in SENT:
|
| 184 |
+
add(out, 'support_ru', st, 'score', 'Какой тон у клиента?', ['негативный', 'нейтральный', 'позитивный'], onehot(SENT, r['sentiment']))
|
| 185 |
+
e = 1.0 if r['human_escalation'] else 0.0
|
| 186 |
+
add(out, 'support_ru', st, 'noul', 'Нужно ли передать обращение человеку?', None, [1 - e, e])
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# 7. nvidia/When2Call (cc-by-4.0): should the assistant call a tool right now?
|
| 190 |
+
def when2call(n_train=3000, n_test=300):
|
| 191 |
+
ds = load_dataset('nvidia/When2Call', 'train_pref', split='train')
|
| 192 |
+
idx = list(range(len(ds))); rng.shuffle(idx)
|
| 193 |
+
for k, i in enumerate(idx[:n_train + n_test]):
|
| 194 |
+
r = ds[i]
|
| 195 |
+
raw = r['tools']
|
| 196 |
+
raw = eval(raw) if isinstance(raw, str) else (raw or [])
|
| 197 |
+
tools = [json.loads(t) if isinstance(t, str) else t for t in raw]
|
| 198 |
+
msgs = eval(r['messages']) if isinstance(r['messages'], str) else r['messages']; user = [m['content'] for m in msgs if m['role'] == 'user'][-1]
|
| 199 |
+
ch = r['chosen_response']
|
| 200 |
+
chosen = (eval(ch) if isinstance(ch, str) else ch)['content']
|
| 201 |
+
tl = '\n'.join(f"- {t['name']}: {t.get('description', '')[:150]}" for t in tools[:8])
|
| 202 |
+
st = f"User request: {user}\nAvailable tools:\n{tl or '(none)'}"
|
| 203 |
+
p = 1.0 if '<TOOLCALL>' in chosen else 0.0
|
| 204 |
+
add('train' if k < n_train else 'test', 'when2call', st, 'noul',
|
| 205 |
+
'Should the assistant call one of the available tools now (instead of answering, asking or declining)?', None, [1 - p, p])
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# 8. Mapika/decider teacher data (Apache-2.0, teacher Qwen3.5-27B): shell risk + routing. Keep teacher-consistent rows.
|
| 209 |
+
DEC = 'https://raw.githubusercontent.com/Mapika/decider/15ab28e8d8e4ec49728165b3426d81de5b77f864/teacher_data/'
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def decider(caps={'commands.jsonl': 4000, 'routing_messages.jsonl': 2500}):
|
| 213 |
+
for f, cap in caps.items():
|
| 214 |
+
lines = urllib.request.urlopen(DEC + f, timeout=120).read().decode().splitlines()
|
| 215 |
+
rng.shuffle(lines)
|
| 216 |
+
n = 0
|
| 217 |
+
for l in lines:
|
| 218 |
+
r = json.loads(l)
|
| 219 |
+
for q in r['questions']:
|
| 220 |
+
if not q.get('teacher_ok'):
|
| 221 |
+
continue
|
| 222 |
+
t, crit, a = q['type'], q.get('criteria'), q['answer']
|
| 223 |
+
if t == 'choice':
|
| 224 |
+
tgt = onehot(list(crit), a)
|
| 225 |
+
elif t == 'noul':
|
| 226 |
+
p = 1.0 if a is True or a == 'true' else 0.0; tgt = [1 - p, p]
|
| 227 |
+
crit = crit if isinstance(crit, dict) and set(crit) == {'false', 'true'} else None
|
| 228 |
+
else:
|
| 229 |
+
tgt = onehot(list(range(len(crit))), int(a))
|
| 230 |
+
out = 'test' if n % 20 == 0 else 'train'
|
| 231 |
+
add(out, 'decider_' + f.split('.')[0], r['state'], t, q['instructions'], crit, tgt)
|
| 232 |
+
n += 1
|
| 233 |
+
if n >= cap:
|
| 234 |
+
break
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# 9. anti-spam ru (apache-2.0).
|
| 238 |
+
def spam(n_train=3000, n_test=300):
|
| 239 |
+
ds = sample(load_dataset('DmitryKRX/anti_spam_ru', split='train'), n_train + n_test)
|
| 240 |
+
for k, r in enumerate(ds):
|
| 241 |
+
p = float(int(r['is_spam']))
|
| 242 |
+
add('train' if k < n_train else 'test', 'spam_ru', r['text'], 'noul', 'Это спам?', None, [1 - p, p])
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# 10. Prompt safety: Nailyk14 (apache/mit rows only) + dmtrdr russian injections (apache) vs benign MASSIVE ru.
|
| 246 |
+
SAFE = {'benign': 'обычный безопасный запрос', 'injection_direct': 'попытка подменить инструкции ассистента',
|
| 247 |
+
'jailbreak': 'попытка обойти ограничения и правила', 'data_exfiltration': 'попытка выведать секреты, ключи, системный промпт',
|
| 248 |
+
'hijack': 'попытка перехватить управление задачей или инструментами'}
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def safety():
|
| 252 |
+
for split, out, n in (('train', 'train', 5000), ('validation', 'calib', 200), ('test', 'test', 400)):
|
| 253 |
+
ds = load_dataset('Nailyk14/prompt-safety-multilingual', split=split)
|
| 254 |
+
ds = ds.filter(lambda r: r['source_license'] in ('apache-2.0', 'mit') and r['label'] in SAFE)
|
| 255 |
+
ru = [i for i, l in enumerate(ds['language']) if l == 'ru']
|
| 256 |
+
other = [i for i, l in enumerate(ds['language']) if l != 'ru']
|
| 257 |
+
rng.shuffle(other)
|
| 258 |
+
pick = (ru + other)[:n]
|
| 259 |
+
for i in pick:
|
| 260 |
+
r = ds[i]
|
| 261 |
+
add(out, 'prompt_safety', r['prompt'], 'choice', 'Что это за запрос к ИИ-ассистенту?', SAFE, onehot(list(SAFE), r['label']))
|
| 262 |
+
d = load_dataset('dmtrdr/russian_prompt_injections', split='train').filter(lambda r: r['source'] != 'openai_synthetic')
|
| 263 |
+
d = sample(d, 2200)
|
| 264 |
+
benign = sample(load_dataset('mteb/MassiveIntentClassification', 'ru', split='train'), 2200)
|
| 265 |
+
for k, (a, b) in enumerate(zip(d, benign)):
|
| 266 |
+
out = 'test' if k % 11 == 0 else 'train'
|
| 267 |
+
text_ru = a['prompt_en'] # fields are swapped in this dataset: prompt_en holds the Russian text
|
| 268 |
+
add(out, 'injection_ru', text_ru, 'noul', 'Это попытка атаки на ИИ-ассистента (обход правил, подмена инструкций)?', None, [0.0, 1.0])
|
| 269 |
+
add(out, 'injection_ru', b['text'], 'noul', 'Это попытка атаки на ИИ-ассистента (обход правил, подмена инструкций)?', None, [1.0, 0.0])
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
if __name__ == '__main__':
|
| 273 |
+
os.makedirs(OUT, exist_ok=True)
|
| 274 |
+
for fn in (typed_decisions, procedural, massive, terra, ai_forever, support, when2call, decider, spam, safety):
|
| 275 |
+
try:
|
| 276 |
+
fn(); print('ok', fn.__name__, flush=True)
|
| 277 |
+
except Exception as e:
|
| 278 |
+
print('FAILED', fn.__name__, repr(e)[:300], flush=True)
|
| 279 |
+
# calib: sources without their own validation split donate 8% of train
|
| 280 |
+
have_calib = {r['source'] for r in recs['calib']}
|
| 281 |
+
keep = []
|
| 282 |
+
for r in recs['train']:
|
| 283 |
+
if r['source'] not in have_calib and rng.random() < 0.08:
|
| 284 |
+
recs['calib'].append(r); stats[('calib', r['source'])] += 1; stats[('train', r['source'])] -= 1
|
| 285 |
+
else:
|
| 286 |
+
keep.append(r)
|
| 287 |
+
recs['train'] = keep
|
| 288 |
+
# balance: cap train per source, test per source
|
| 289 |
+
for sp, cap in (('train', CAP_TRAIN), ('test', 300)):
|
| 290 |
+
by = collections.defaultdict(list)
|
| 291 |
+
for r in recs[sp]:
|
| 292 |
+
by[r['source']].append(r)
|
| 293 |
+
recs[sp] = []
|
| 294 |
+
for src, rs in by.items():
|
| 295 |
+
rng.shuffle(rs)
|
| 296 |
+
c = cap.get(src, cap['*']) if isinstance(cap, dict) else cap
|
| 297 |
+
recs[sp] += rs[:c]
|
| 298 |
+
stats[(sp, src)] = min(len(rs), c)
|
| 299 |
+
for sp, rs in recs.items():
|
| 300 |
+
rng.shuffle(rs)
|
| 301 |
+
with open(os.path.join(OUT, f'pub_{sp}.jsonl'), 'w', encoding='utf-8') as f:
|
| 302 |
+
for r in rs:
|
| 303 |
+
f.write(json.dumps(r, ensure_ascii=False) + '\n')
|
| 304 |
+
for sp in ('train', 'calib', 'test'):
|
| 305 |
+
print(sp, len(recs[sp]), {s: c for (p, s), c in sorted(stats.items()) if p == sp})
|
training/build_skills_mix.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Assemble synthetic skill-routing questions (with a `none` option) and merge them into the public mix.
|
| 2 |
+
|
| 3 |
+
Input: data/skills_syn/{catalogs,messages,negatives}.json (gen_skills.py) + data/pub/pub_*.jsonl.
|
| 4 |
+
Output: data/pub2/pub_{train,calib,test}.jsonl. Held-out catalogs go to test/calib only, so the
|
| 5 |
+
test measures routing over skills the model never saw.
|
| 6 |
+
"""
|
| 7 |
+
import json, random, os, collections
|
| 8 |
+
rng = random.Random(20260926)
|
| 9 |
+
D = os.environ.get('DATA_DIR', 'data')
|
| 10 |
+
cats = json.load(open(f'{D}/skills_syn/catalogs.json', encoding='utf-8'))
|
| 11 |
+
msgs = json.load(open(f'{D}/skills_syn/messages.json', encoding='utf-8'))
|
| 12 |
+
negs = json.load(open(f'{D}/skills_syn/negatives.json', encoding='utf-8'))
|
| 13 |
+
|
| 14 |
+
NONE_DESC_RU = 'навык не нужен: обычный разговор, вопрос или задача без специального навыка'
|
| 15 |
+
NONE_DESC_EN = 'no skill needed: small talk, a general question or a task without a special skill'
|
| 16 |
+
INS_RU = 'Какой навык ассистент должен загрузить, чтобы выполнить это сообщение пользователя? Если специальный навык не нужен, выбери none.'
|
| 17 |
+
INS_EN = 'Which skill should the assistant load to handle this user message? Choose none if no special skill is needed.'
|
| 18 |
+
NOUL_RU = 'Нужен ли ассистенту специальный навык (инструмент), чтобы выполнить это сообщение?'
|
| 19 |
+
|
| 20 |
+
doms = [d for d in cats if d != 'capyagent']
|
| 21 |
+
rng.shuffle(doms)
|
| 22 |
+
held = set(doms[:6]) # unseen catalogs -> test
|
| 23 |
+
calib_doms = set(doms[6:9]) # unseen catalogs -> calib
|
| 24 |
+
def split_of(dom):
|
| 25 |
+
if dom in held:
|
| 26 |
+
return 'test'
|
| 27 |
+
if dom in calib_doms:
|
| 28 |
+
return 'calib'
|
| 29 |
+
if dom == 'capyagent':
|
| 30 |
+
r = rng.random()
|
| 31 |
+
return 'test' if r < 0.15 else 'calib' if r < 0.22 else 'train'
|
| 32 |
+
return 'train'
|
| 33 |
+
|
| 34 |
+
def is_ru(t):
|
| 35 |
+
return sum('а' <= ch.lower() <= 'я' for ch in t) > len(t) * 0.2
|
| 36 |
+
|
| 37 |
+
out = collections.defaultdict(list)
|
| 38 |
+
def rec(split, dom, text, gold, drop_gold=False):
|
| 39 |
+
cat = cats[dom]
|
| 40 |
+
k = rng.randint(6, 16)
|
| 41 |
+
pool = [s for s in cat if s != gold]
|
| 42 |
+
opts = rng.sample(pool, min(k - 2, len(pool)))
|
| 43 |
+
if gold != 'none' and not drop_gold:
|
| 44 |
+
opts.append(gold)
|
| 45 |
+
ru = is_ru(text)
|
| 46 |
+
crit = {o: cat[o] for o in opts}
|
| 47 |
+
crit['none'] = NONE_DESC_RU if ru else NONE_DESC_EN
|
| 48 |
+
keys = list(crit); rng.shuffle(keys); crit = {k_: crit[k_] for k_ in keys}
|
| 49 |
+
g = 'none' if (gold == 'none' or drop_gold) else gold
|
| 50 |
+
src = 'skills_capy' if dom == 'capyagent' else 'skills_syn'
|
| 51 |
+
out[split].append({'state': text, 'q': {'t': 'choice', 'ins': INS_RU if ru else INS_EN, 'crit': crit},
|
| 52 |
+
'opts': list(crit), 'target': [1.0 if o == g else 0.0 for o in crit], 'gold': g, 'source': src})
|
| 53 |
+
if rng.random() < 0.2:
|
| 54 |
+
p = 0.0 if g == 'none' else 1.0
|
| 55 |
+
out[split].append({'state': text, 'q': {'t': 'noul', 'ins': NOUL_RU, 'crit': None}, 'opts': ['false', 'true'],
|
| 56 |
+
'target': [1 - p, p], 'gold': 'true' if p else 'false', 'source': src + '_need'})
|
| 57 |
+
|
| 58 |
+
for m in msgs:
|
| 59 |
+
if m['domain'] not in cats or m['skill'] not in cats[m['domain']]:
|
| 60 |
+
continue
|
| 61 |
+
rec(split_of(m['domain']), m['domain'], m['text'], m['skill'], drop_gold=rng.random() < 0.08)
|
| 62 |
+
for n in negs:
|
| 63 |
+
dom = n['domain'] if n['domain'] in cats else rng.choice(list(cats))
|
| 64 |
+
rec(split_of(dom), dom, n['text'], 'none')
|
| 65 |
+
|
| 66 |
+
stats = {sp: collections.Counter(r['source'] for r in rs) for sp, rs in out.items()}
|
| 67 |
+
nones = {sp: sum(r['gold'] == 'none' for r in rs if r['q']['t'] == 'choice') for sp, rs in out.items()}
|
| 68 |
+
print('held-out test catalogs:', sorted(held)); print('calib catalogs:', sorted(calib_doms))
|
| 69 |
+
print(stats, 'none per split:', nones)
|
| 70 |
+
os.makedirs(f'{D}/pub2', exist_ok=True)
|
| 71 |
+
for sp in ('train', 'calib', 'test'):
|
| 72 |
+
rows = [json.loads(l) for l in open(f'{D}/pub/pub_{sp}.jsonl', encoding='utf-8')] + out[sp]
|
| 73 |
+
rng.shuffle(rows)
|
| 74 |
+
with open(f'{D}/pub2/pub_{sp}.jsonl', 'w', encoding='utf-8') as f:
|
| 75 |
+
for r in rows:
|
| 76 |
+
f.write(json.dumps(r, ensure_ascii=False) + '\n')
|
| 77 |
+
print(sp, len(rows))
|
training/gen_skills.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Synthetic skill-routing data for the public Kivok/Migom (generator: DeepSeek Flash, MIT open weights).
|
| 2 |
+
|
| 3 |
+
Stages (each cached to data/skills_syn/*.json, rerun resumes):
|
| 4 |
+
catalogs - fictional assistant skill catalogs across domains (+ the real CapyAgent catalog)
|
| 5 |
+
messages - user messages that need a given skill (ru ~75%, en ~25%)
|
| 6 |
+
negatives - messages that need no skill: small talk, general knowledge, and hard negatives
|
| 7 |
+
that mention a catalog's topics without asking for the action
|
| 8 |
+
Usage: python3 gen_skills.py [catalogs|messages|negatives|all]
|
| 9 |
+
"""
|
| 10 |
+
import json, os, re, sys, random, threading, time, urllib.request
|
| 11 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 12 |
+
|
| 13 |
+
OUT = os.environ.get('OUT_DIR', 'data/skills_syn')
|
| 14 |
+
os.makedirs(OUT, exist_ok=True)
|
| 15 |
+
MODEL = 'deepseek-flash'
|
| 16 |
+
KEY = os.environ['DEEPSEEK_API_KEY']
|
| 17 |
+
lock = threading.Lock()
|
| 18 |
+
usage = {'in': 0, 'out': 0, 'calls': 0}
|
| 19 |
+
|
| 20 |
+
DOMAINS = [
|
| 21 |
+
'интернет-магазин одежды', 'стоматологическая клиника', 'автосервис', 'агентство недвижимости', 'фитнес-клуб',
|
| 22 |
+
'бухгалтерия малого бизнеса', 'IT-отдел компании (helpdesk)', 'DevOps и сервера', 'личный ассистент для дома и семьи',
|
| 23 |
+
'умный дом', 'школа английского онлайн', 'служба доставки еды', 'турагентство', 'юридическая консультация',
|
| 24 |
+
'HR и найм', 'маркетинговое агентство', 'SMM и соцсети', 'интернет-провайдер (поддержка)', 'банк (поддержка клиентов)',
|
| 25 |
+
'страховая компания', 'логистика и склад', 'производство (заводской цех)', 'ресторан', 'салон красоты',
|
| 26 |
+
'ветеринарная клиника', 'онлайн-кинотеатр', 'игровая студия', 'новостная редакция', 'исследователь и учёный',
|
| 27 |
+
'разработчик-фрилансер', 'продажи B2B и CRM', 'личные финансы', 'путешествия и поездки', 'здоровье и питание',
|
| 28 |
+
'образование для школьников', 'музыкант и продюсер', 'видеомонтаж и YouTube', 'аналитик данных', 'кибербезопасность',
|
| 29 |
+
'государственные услуги и документы',
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def chat(prompt, temperature=1.0, max_tokens=3000, retries=4):
|
| 34 |
+
body = {'model': MODEL, 'messages': [{'role': 'user', 'content': prompt}], 'temperature': temperature,
|
| 35 |
+
'max_tokens': max_tokens, 'response_format': {'type': 'json_object'}}
|
| 36 |
+
for i in range(retries):
|
| 37 |
+
try:
|
| 38 |
+
req = urllib.request.Request('https://api.deepseek.com/chat/completions', json.dumps(body).encode(),
|
| 39 |
+
{'Authorization': 'Bearer ' + KEY, 'Content-Type': 'application/json'})
|
| 40 |
+
d = json.load(urllib.request.urlopen(req, timeout=180))
|
| 41 |
+
with lock:
|
| 42 |
+
usage['in'] += d['usage']['prompt_tokens']; usage['out'] += d['usage']['completion_tokens']; usage['calls'] += 1
|
| 43 |
+
return json.loads(d['choices'][0]['message']['content'])
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print('retry', i, repr(e)[:150], flush=True)
|
| 46 |
+
time.sleep(3 * (i + 1))
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def cached(name, fn):
|
| 51 |
+
p = os.path.join(OUT, name)
|
| 52 |
+
if os.path.exists(p):
|
| 53 |
+
return json.load(open(p, encoding='utf-8'))
|
| 54 |
+
v = fn()
|
| 55 |
+
json.dump(v, open(p, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
|
| 56 |
+
return v
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def catalogs():
|
| 60 |
+
def one(dom):
|
| 61 |
+
r = chat(f"""Придумай каталог навыков (skills) для ИИ-ассистента в сфере: «{dom}».
|
| 62 |
+
Навык - это отдельная возможность, которую ассистент подгружает, чтобы выполнить задачу (например работа с календарём, выставление счёта, поиск по базе заказов).
|
| 63 |
+
Верни JSON: {{"skills": [{{"name": "kebab-case-имя-латиницей", "description": "короткое описание на русском, 5-15 слов"}}]}}
|
| 64 |
+
Ровно 16 навыков, разные по смыслу, без пересечений. Некоторые описания можно делать на английском (примерно 1 из 4).""", temperature=0.9)
|
| 65 |
+
if r and isinstance(r.get('skills'), list):
|
| 66 |
+
return dom, {s['name']: s['description'] for s in r['skills'] if s.get('name') and s.get('description')}
|
| 67 |
+
return dom, None
|
| 68 |
+
res = {}
|
| 69 |
+
with ThreadPoolExecutor(8) as ex:
|
| 70 |
+
for dom, cat in ex.map(one, DOMAINS):
|
| 71 |
+
if cat:
|
| 72 |
+
res[dom] = cat
|
| 73 |
+
res['capyagent'] = json.load(open(os.environ['EXTRA_CATALOG'], encoding='utf-8')) if os.environ.get('EXTRA_CATALOG') else {}
|
| 74 |
+
return res
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def messages(cats):
|
| 78 |
+
jobs = []
|
| 79 |
+
for dom, cat in cats.items():
|
| 80 |
+
items = list(cat.items())
|
| 81 |
+
for i in range(0, len(items), 4):
|
| 82 |
+
jobs.append((dom, items[i:i + 4]))
|
| 83 |
+
|
| 84 |
+
def one(job):
|
| 85 |
+
dom, items = job
|
| 86 |
+
spec = '\n'.join(f'- {n}: {d}' for n, d in items)
|
| 87 |
+
r = chat(f"""Сфера ассистента: «{dom}». Вот навыки:
|
| 88 |
+
{spec}
|
| 89 |
+
Для КАЖДОГО навыка напиши 9 разных сообщений пользователя, для выполнения которых ассистенту нужен именно этот навык:
|
| 90 |
+
6 на русском и 3 на английском. Делай их живыми и разнообразными: короткие и длинные, вежливые и грубые, с опечатками и без,
|
| 91 |
+
иногда с лишними подробностями, иногда косвенно (не называй навык прямо), иногда с упоминанием другой темы, но главная просьба - про этот навык.
|
| 92 |
+
Верни JSON: {{"items": [{{"skill": "имя навыка", "messages": ["...", "..."]}}]}}""", temperature=1.0, max_tokens=4000)
|
| 93 |
+
out = []
|
| 94 |
+
if r:
|
| 95 |
+
for it in r.get('items', []):
|
| 96 |
+
if it.get('skill') in dict(items):
|
| 97 |
+
out += [{'domain': dom, 'skill': it['skill'], 'text': m} for m in it.get('messages', []) if isinstance(m, str) and len(m) > 3]
|
| 98 |
+
return out
|
| 99 |
+
res = []
|
| 100 |
+
with ThreadPoolExecutor(8) as ex:
|
| 101 |
+
for k, o in enumerate(ex.map(one, jobs)):
|
| 102 |
+
res += o
|
| 103 |
+
if k % 20 == 0:
|
| 104 |
+
print('messages', k, len(jobs), len(res), usage, flush=True)
|
| 105 |
+
return res
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def negatives(cats):
|
| 109 |
+
jobs = [('general', None)] * 30 + [(dom, cat) for dom, cat in cats.items()]
|
| 110 |
+
|
| 111 |
+
def one(job):
|
| 112 |
+
dom, cat = job
|
| 113 |
+
if cat is None:
|
| 114 |
+
p = """Напиши 25 разных сообщений пользователя ИИ-ассистенту, для которых НЕ нужен никакой специальный навык или инструмент:
|
| 115 |
+
болтовня, приветствия, благодарности, общие вопросы из головы (факты, объяснения, советы), просьбы переформулировать или придумать текст,
|
| 116 |
+
мнения, шутки, короткие реакции («ок», «понял, спасибо»). 18 на русском, 7 на английском. Разная длина и стиль.
|
| 117 |
+
Верни JSON: {"messages": ["...", "..."]}"""
|
| 118 |
+
else:
|
| 119 |
+
spec = '\n'.join(f'- {n}: {d}' for n, d in list(cat.items())[:16])
|
| 120 |
+
p = f"""Ассистент в сфере «{dom}» умеет вот это (навыки):
|
| 121 |
+
{spec}
|
| 122 |
+
Напиши 16 сообщений пользователя, которые КАСАЮТСЯ тем этих навыков, но НЕ требуют их запускать:
|
| 123 |
+
рассказ о себе, эмоция, благодарность за прошлую работу, общий вопрос «как это вообще устроено», мнение, шутка, уточнение, не требующее действия.
|
| 124 |
+
12 на русском, 4 на английском. Верни JSON: {{"messages": ["...", "..."]}}"""
|
| 125 |
+
r = chat(p, temperature=1.0)
|
| 126 |
+
return [{'domain': dom if cat else None, 'text': m} for m in (r or {}).get('messages', []) if isinstance(m, str) and len(m) > 1]
|
| 127 |
+
res = []
|
| 128 |
+
with ThreadPoolExecutor(8) as ex:
|
| 129 |
+
for o in ex.map(one, jobs):
|
| 130 |
+
res += o
|
| 131 |
+
return res
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
if __name__ == '__main__':
|
| 135 |
+
stage = sys.argv[1] if len(sys.argv) > 1 else 'all'
|
| 136 |
+
cats = cached('catalogs.json', catalogs)
|
| 137 |
+
print('catalogs', len(cats), sum(len(c) for c in cats.values()), usage, flush=True)
|
| 138 |
+
if stage in ('messages', 'all'):
|
| 139 |
+
msgs = cached('messages.json', lambda: messages(cats))
|
| 140 |
+
print('messages', len(msgs), usage, flush=True)
|
| 141 |
+
if stage in ('negatives', 'all'):
|
| 142 |
+
neg = cached('negatives.json', lambda: negatives(cats))
|
| 143 |
+
print('negatives', len(neg), usage, flush=True)
|
| 144 |
+
print('USAGE', usage)
|
training/to_jevk5_pub.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Convert the public mix to JevK5 LoRA format (subset: Migom trains ~0.5 items/s on Arc)."""
|
| 2 |
+
import json, random, collections, sys, os
|
| 3 |
+
src_dir, out_dir, per_src_train, per_src_dev = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4])
|
| 4 |
+
rng = random.Random(7)
|
| 5 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 6 |
+
def conv(r, i):
|
| 7 |
+
q = r['q']; t = q['t']
|
| 8 |
+
keys = ['false', 'true'] if t == 'noul' else r['opts']
|
| 9 |
+
it = {'id': f"{r['source']}-{i}", 'source': r['source'], 'state': r['state'],
|
| 10 |
+
'question': {'type': t, 'instructions': q['ins'], 'criteria': q['crit']}, 'expected': r['gold']}
|
| 11 |
+
if max(r['target']) < 0.999:
|
| 12 |
+
it['gold_probs'] = {k: v for k, v in zip(keys, r['target'])}
|
| 13 |
+
return it
|
| 14 |
+
for name, out, cap in (('pub_train', 'train', per_src_train), ('pub_calib', 'dev', per_src_dev)):
|
| 15 |
+
by = collections.defaultdict(list)
|
| 16 |
+
for l in open(os.path.join(src_dir, name + '.jsonl'), encoding='utf-8'):
|
| 17 |
+
r = json.loads(l); by[r['source']].append(r)
|
| 18 |
+
rows = []
|
| 19 |
+
for s, rs in by.items():
|
| 20 |
+
rng.shuffle(rs); rows += rs[:cap]
|
| 21 |
+
rng.shuffle(rows)
|
| 22 |
+
with open(os.path.join(out_dir, out + '.jsonl'), 'w', encoding='utf-8') as f:
|
| 23 |
+
for i, r in enumerate(rows): f.write(json.dumps(conv(r, i), ensure_ascii=False) + '\n')
|
| 24 |
+
print(out, len(rows))
|
training/train_decider_lora.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LoRA fine-tune of Mapika/decider-2b on our public typed-decision mix (JevK5-format jsonl: state/question/expected).
|
| 2 |
+
|
| 3 |
+
Uses decider's own prompt builder (plain state-first layout, "Answer: (" slot, option-letter logits), so the
|
| 4 |
+
trained model is served unchanged by decider.infer.Decider. Score questions are trained as lists (isolated=False).
|
| 5 |
+
Usage: python train_decider_lora.py BASE_DIR DATA_DIR OUT_DIR [--epochs 1 --lr 1e-4 --rank 32]
|
| 6 |
+
"""
|
| 7 |
+
import argparse, json, os, random, shutil, sys, time, math
|
| 8 |
+
import torch
|
| 9 |
+
from peft import LoraConfig, get_peft_model
|
| 10 |
+
from decider.model import DecisionModel, collate
|
| 11 |
+
from decider.prompt import build
|
| 12 |
+
from decider.data.core import Example, Q
|
| 13 |
+
from decider.train import batches_by_tokens, loss_fn
|
| 14 |
+
|
| 15 |
+
ap = argparse.ArgumentParser()
|
| 16 |
+
ap.add_argument('base'); ap.add_argument('data'); ap.add_argument('out')
|
| 17 |
+
ap.add_argument('--epochs', type=float, default=1.0); ap.add_argument('--lr', type=float, default=1e-4)
|
| 18 |
+
ap.add_argument('--rank', type=int, default=32); ap.add_argument('--max-tokens', type=int, default=8192)
|
| 19 |
+
ap.add_argument('--max-ctx', type=int, default=1536); ap.add_argument('--limit', type=int, default=0)
|
| 20 |
+
ap.add_argument('--dtype', default='bf16', choices=['bf16', 'fp16']); ap.add_argument('--device', default='cuda')
|
| 21 |
+
ap.add_argument('--max-minutes', type=float, default=0, help='stop training after this many minutes and save')
|
| 22 |
+
ap.add_argument('--adapter-out', default='', help='also save the LoRA adapter here')
|
| 23 |
+
ap.add_argument('--version', default='migom-2b-decider-pub1')
|
| 24 |
+
a = ap.parse_args()
|
| 25 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 26 |
+
rng = random.Random(0)
|
| 27 |
+
dev = a.device
|
| 28 |
+
DT = torch.bfloat16 if a.dtype == 'bf16' else torch.float16
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def to_example(r):
|
| 32 |
+
q = r['question']; t = q['type']; crit = q.get('criteria'); gold = str(r['expected'])
|
| 33 |
+
if t == 'choice':
|
| 34 |
+
crit = dict.fromkeys(crit) if isinstance(crit, list) else crit
|
| 35 |
+
names = list(crit); opts = [n if crit[n] in (None, '') else f'{n}: {crit[n]}' for n in names]
|
| 36 |
+
gi = names.index(gold) if gold in names else -1
|
| 37 |
+
elif t == 'score':
|
| 38 |
+
opts = [f'{i}: {c}' for i, c in enumerate(crit)]; gi = int(gold)
|
| 39 |
+
else:
|
| 40 |
+
c = crit or {}
|
| 41 |
+
f, tr = c.get('false'), c.get('true')
|
| 42 |
+
opts = ['no' if not f else f'no: {f}', 'yes' if not tr else f'yes: {tr}']
|
| 43 |
+
gi = 1 if gold == 'true' else 0
|
| 44 |
+
if gi < 0:
|
| 45 |
+
return None
|
| 46 |
+
return Example(context=r['state'], qs=[Q(text=q['instructions'], options=opts, gold=gi)], task=r.get('source', ''))
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def load(name):
|
| 50 |
+
ex = [to_example(json.loads(l)) for l in open(os.path.join(a.data, name), encoding='utf-8')]
|
| 51 |
+
ex = [e for e in ex if e]
|
| 52 |
+
return ex[:a.limit] if a.limit else ex
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
t0 = time.time()
|
| 56 |
+
m = DecisionModel(a.base, dtype=DT, grad_ckpt=True)
|
| 57 |
+
tok = m.tok
|
| 58 |
+
names = {n.rsplit('.', 1)[-1] for n, _ in m.lm.named_modules()}
|
| 59 |
+
targets = [t for t in ('q_proj', 'k_proj', 'v_proj', 'o_proj', 'in_proj_qkvz', 'in_proj_ba', 'in_proj_qkv', 'in_proj_z',
|
| 60 |
+
'in_proj_b', 'in_proj_a', 'out_proj', 'gate_proj', 'up_proj', 'down_proj') if t in names]
|
| 61 |
+
peft_model = get_peft_model(m.lm, LoraConfig(r=a.rank, lora_alpha=2 * a.rank, lora_dropout=0.05, target_modules=targets))
|
| 62 |
+
peft_model.print_trainable_parameters()
|
| 63 |
+
m.lm = peft_model.base_model.model # same CausalLM with LoRA layers injected in place; DecisionModel keeps calling .model / .lm_head
|
| 64 |
+
m.to(dev)
|
| 65 |
+
|
| 66 |
+
train = load('train.jsonl'); dev_ex = load('dev.jsonl')
|
| 67 |
+
items = [build(e, tok, rng, max_options=255, max_ctx_tokens=a.max_ctx) for e in train]
|
| 68 |
+
dev_items = [build(e, tok, random.Random(1), max_options=255, max_ctx_tokens=a.max_ctx) for e in dev_ex]
|
| 69 |
+
print('train', len(items), 'dev', len(dev_items), flush=True)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@torch.no_grad()
|
| 73 |
+
def evaluate():
|
| 74 |
+
m.eval(); ok = n = 0
|
| 75 |
+
for idx in batches_by_tokens(dev_items, a.max_tokens, random.Random(2)):
|
| 76 |
+
b = collate([dev_items[i] for i in idx], tok.pad_token_id)
|
| 77 |
+
bt = {k: v.to(dev) if torch.is_tensor(v) else v for k, v in b.items()}
|
| 78 |
+
lg = m(bt); g = bt['golds']; msk = g >= 0
|
| 79 |
+
ok += int((lg[msk].argmax(-1) == g[msk]).sum()); n += int(msk.sum())
|
| 80 |
+
m.train(); return ok / max(1, n)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
print('before dev acc', round(evaluate(), 4), flush=True)
|
| 84 |
+
params = [p for p in m.parameters() if p.requires_grad]
|
| 85 |
+
opt = torch.optim.AdamW(params, lr=a.lr, weight_decay=0.0)
|
| 86 |
+
plan = []
|
| 87 |
+
for ep in range(math.ceil(a.epochs)):
|
| 88 |
+
plan += batches_by_tokens(items, a.max_tokens, random.Random(10 + ep))
|
| 89 |
+
total = int(len(plan) * a.epochs / math.ceil(a.epochs))
|
| 90 |
+
scaler = torch.amp.GradScaler('cuda', enabled=a.dtype == 'fp16')
|
| 91 |
+
sch = torch.optim.lr_scheduler.LambdaLR(opt, lambda s: min(1.0, (s + 1) / 20) * 0.5 * (1 + math.cos(math.pi * s / total)))
|
| 92 |
+
m.train(); win = []
|
| 93 |
+
for step, idx in enumerate(plan[:total]):
|
| 94 |
+
b = collate([items[i] for i in idx], tok.pad_token_id)
|
| 95 |
+
bt = {k: v.to(dev) if torch.is_tensor(v) else v for k, v in b.items()}
|
| 96 |
+
loss, ce = loss_fn(m(bt).float(), bt['golds'], bt['nopts'])
|
| 97 |
+
opt.zero_grad(set_to_none=True); scaler.scale(loss).backward(); scaler.unscale_(opt)
|
| 98 |
+
torch.nn.utils.clip_grad_norm_(params, 1.0); scaler.step(opt); scaler.update(); sch.step()
|
| 99 |
+
win.append(loss.item())
|
| 100 |
+
if a.max_minutes and time.time() - t0 > 60 * a.max_minutes:
|
| 101 |
+
print(f'time limit: stopping at step {step + 1}/{total}', flush=True); break
|
| 102 |
+
if (step + 1) % 25 == 0:
|
| 103 |
+
eta = (total - step - 1) * (time.time() - t0) / (step + 1) / 60
|
| 104 |
+
print(f'step {step + 1}/{total} loss {sum(win) / len(win):.4f} ~{eta:.0f} min left', flush=True); win = []
|
| 105 |
+
print('after dev acc', round(evaluate(), 4), flush=True)
|
| 106 |
+
if a.adapter_out:
|
| 107 |
+
peft_model.save_pretrained(a.adapter_out)
|
| 108 |
+
merged = peft_model.merge_and_unload()
|
| 109 |
+
os.makedirs(a.out, exist_ok=True)
|
| 110 |
+
merged.save_pretrained(a.out); tok.save_pretrained(a.out)
|
| 111 |
+
cfg = json.load(open(os.path.join(a.base, 'decider_config.json')))
|
| 112 |
+
cfg.update(version=a.version, parent=cfg.get('version'), stage=f'LoRA r{a.rank} on the open typed-decision mix, {a.epochs} epoch(s)')
|
| 113 |
+
json.dump(cfg, open(os.path.join(a.out, 'decider_config.json'), 'w'), indent=1)
|
| 114 |
+
print('done in', int(time.time() - t0), 's', flush=True)
|