Репозиторий собран из моих рабочих Jupyter-ноутбуков, которые я вёл при адаптации большой языковой модели к татарскому языку в контуре Cloud.ru: каждый этап адаптации — это отдельный ноутбук, запускаемый на одной A100 80GB. Пайплайн покрывает перенос словаря (trans-tokenization), continual pretraining, instruction tuning (LoRA SFT) и согласование с предпочтениями (DPO), вместе с evaluation pipeline из шести функциональных метрик. Весь конфиг через один YAML.
M0 — Базовая модель (Mistral-NeMo-12B-Base)
│
│ E1.1 trans-tokenization 32k BPE (татарский) + alignment fast_align (ru↔tt)
│ → перенос эмбеддингов взвешенным средним исходных токенов
▼
M1-init — модель с татарским словарём
│
│ E1.2 continual pretraining partial-FT: embed + lm_head + norm + первые/последние K блоков
▼
M1 — адаптирована к языку
│
│ E2 SFT с LoRA инструкционные диалоги, assistant-only loss → merge
▼
M2 — инструкционная модель
│
│ E3 DPO с LoRA preference-пары, reference = adapter-off база → merge
▼
M3 — согласована с предпочтениями носителей
│
▼
[Eval harness] ← 6 метрик на каждом этапе (baseline / M1 / M2 / M3) → MLflow
- E1.1 — перенос словаря (
vocabulary/). Обучается собственный 32k BPE-токенайзер на татарском корпусе; эмбеддинги татарских токенов инициализируются взвешенным средним эмбеддингов соответствующих токенов исходной модели по параллельному корпусу предложений ru↔tt. - E1.2 — continual pretraining (
pretraining/). Partial fine-tuning на татарском моно-корпусе: размораживаютсяembed_tokens,lm_head, финальныйnormи первые/последние K decoder-блоков. Восстанавливает языковое моделирование под новый словарь. - E2 — SFT с LoRA (
sft/). Supervised fine-tuning на инструкционных диалогах с LoRA (r=64, α=32), маскированием loss по ответам ассистента (assistant_only_loss); адаптер мёржится в базу — получается standalone-чекпоинт M2. - E3 — DPO с LoRA (
dpo/). Direct Preference Optimization на размеченных preference-парах; - Eval (
eval/). Шесть метрик на каждом этапе, результаты логируются в MLflow (необходимо поднять сервис на базе Cloud.ru).
Harness замеряет шесть функциональных метрик:
| Метрика | Что измеряет |
|---|---|
PPL_native |
cross-tokenizer perplexity - качество языкового моделирования |
fertility (f_τ) |
средняя фрагментация: число токенов на 1 слово целевым токенайзером |
IFEval-tt prompt-strict |
доля промптов, где выполнены все verifiable-инструкции (длина, ключевые слова, регистр и т. п.) |
DPO margin |
средняя разность неявного reward между chosen и rejected на held-out preference-парах |
DPO accuracy |
доля пар, где chosen получает больший неявный reward, чем rejected |
LCWR |
length-controlled win rate против SFT-чекпоинта |
Сводная динамика по этапам:
| Этап | PPL_native ↓ | f_τ ↓ | IFEval-tt ↑ | margin ↑ | accuracy ↑ | LCWR ↑ |
|---|---|---|---|---|---|---|
| Базовая модель (M0) | 58.4 | 4.2 | 2.5 %* | — | — | — |
| + trans-tok + CPT (M1) | 11.0 | 1.6 | 14.0 %* | — | — | — |
| + SFT с LoRA (M2) | 10.9 | 1.6 | 44.5 % | 0 | 0.5 | 35.8 %** |
| + DPO (M3) | 10.9 | 1.6 | 49.0 % | 1.23 | 0.68 | 64.2 %** |
* IFEval-tt prompt-strict в режиме 5-shot (базовая модель не инструкционная); на M2/M3 — zero-shot. ** LCWR — доля побед в парном сравнении M3 против M2 (length-controlled);
Все входы — локальные JSONL (по одному объекту на строку); пути задаются в configs/default.yaml.
Имена полей конфигурируемы, ниже — значения по умолчанию.
Моно-татарский корпус — tokenizer_corpus_jsonl (обучение BPE) и cpt_corpus_jsonl
(continual pretraining). Источник: CulturaX (tt).
{"text": "Татар теле — тюрки телләре гаиләсенә керүче милли тел."}Параллельный корпус ru↔tt — parallel_corpus_jsonl (alignment для trans-tokenization).
Источник: yasalma/tt-ru-mt (~600k пар).
{"ru": "Доброе утро", "tt": "Хәерле иртә"}Инструкционные диалоги — sft_corpus_jsonl (E2). Последний ход — обязательно assistant;
system допускается только первым сообщением.
{"messages": [{"role": "user", "content": "Казан турында кыскача сөйлә."}, {"role": "assistant", "content": "Казан — Татарстан Республикасының башкаласы, Идел буенда урнашкан."}]}Preference-пары — dpo_pairs_jsonl (E3). Поля prompt/chosen/rejected — единообразно либо
строки, либо списки сообщений (для применения того же [INST]-шаблона, что в SFT).
{"prompt": "Балаларга кыска әкият яз.", "chosen": "Борын-борын заманда бер куян яшәгән...", "rejected": "Әкият."}PPL / fertility — ppl_eval_jsonl, fertility_eval_jsonl. Источник: FLORES-200 (tat_Cyrl),
997 пар / срез CulturaX.
{"text": "Идел елгасы Татарстан территориясе аша ага."}IFEval-tt — ifeval_tt_jsonl. Каждый промпт сопровождается машинно-проверяемыми ограничениями
(min_words, max_words, keyword_include, bullet_list, json_format и др.).
{"prompt": "Казан турында кимендә 50 сүздән торган текст яз, 'Идел' сүзен кертеп.", "constraints": [{"type": "min_words", "count": 50}, {"type": "keyword_include", "keywords": ["Идел"]}]}Few-shot примеры для IFEval — ifeval_fewshot_jsonl (используются для 5-shot замера на baseline/M1;
держатся отдельно от тестового набора).
{"prompt": "Өч җөмләдән торган җавап яз.", "response": "Беренче җөмлә. Икенче җөмлә. Өченче җөмлә."}Held-out preference — pref_val_jsonl (DPO margin/accuracy). Схема как у dpo_pairs_jsonl.
{"prompt": "Сәламләү яз.", "chosen": "Исәнмесез! Хәлләрегез ничек?", "rejected": "Привет."}LCWR-промпты — lcwr_prompts_jsonl (open-ended инструкции для сравнения M3 против M2).
{"prompt": "Балаларга татар телен ничек өйрәтергә? Киңәшләр бир."}LCWR-метки — lcwr_labels_jsonl (генерируется export_lcwr_pairs, затем носители проставляют
win: 1 — победил кандидат M3, 0 — baseline M2).
{"id": 0, "prompt": "...", "candidate_response": "...", "baseline_response": "...", "win": 1}| Слой | Технология |
|---|---|
| Базовая модель | Mistral-NeMo-12B-Base (configurable) |
| Обучение / inference | PyTorch 2.8, Transformers 4.57, TRL 0.24, PEFT 0.17 |
| PEFT-метод | LoRA (r=64, α=32) |
| Перенос словаря | SentencePiece (32k BPE), fast_align (FremyCompany fork) |
| Оптимизатор E1.2 | paged AdamW 8-bit (bitsandbytes) |
| Метрики | NumPy, statsmodels (логистическая GLM для LCWR) |
| Трекинг | MLflow |
| Конфиг | pydantic-settings + YAML |
| Запуск | Jupyter-ноутбуки на cloud.ru (по одному на эксперимент) |
| Инфраструктура | Docker (CUDA 12.6) → cloud.ru ML Space, 1× NVIDIA A100 80GB |
mistral-low-resource-domain/
├── src/mistral_lrd/
│ ├── config.py # pydantic-settings: все пути и гиперпараметры, load_settings()
│ ├── modeling.py # загрузка модели/токенайзера (bf16, attn, pad=eos)
│ ├── chat_template.py # [INST]-шаблон + {% generation %} для assistant-only loss
│ ├── freezing.py # partial-FT: какие слои разморозить (E1.2)
│ ├── peft_utils.py # merge адаптера в базу → standalone-чекпоинт
│ ├── tracking.py # MLflow: run на этап, log_params/log_metrics
│ ├── vocabulary/ # E1.1 trans-tokenization
│ │ ├── train_tokenizer.py # 32k BPE на татарском (SentencePiece)
│ │ ├── parallel_corpus.py # Moses-корпус ru↔tt + детекция префиксов токенайзера
│ │ ├── align.py # fast_align (-p) + префлайт бинаря
│ │ ├── token_mapping.py # token→token mapping из таблицы alignment
│ │ ├── remap.py # перенос эмбеддингов взвешенным средним
│ │ └── graft.py # оркестрация E1.1 → M1-init
│ ├── pretraining/ # E1.2 continual pretraining (partial-FT)
│ ├── sft/ # E2 LoRA SFT
│ ├── dpo/ # E3 LoRA DPO
│ ├── eval/ # 6 метрик: perplexity, fertility, ifeval_tt, dpo_metrics, lcwr
│ │ ├── lcwr_export.py # генерация ответов M3/M2 для разметки носителями
│ │ └── run_eval.py # run_eval(cfg, stage) → метрики этапа в MLflow
│ └── data_prep/ # подготовка публичных датасетов из локальных parquet
├── scripts/data_prep/ # CLI-обёртки: build_culturax / build_parallel / build_flores + README
├── notebooks/ # e1_vocabulary_transfer · e1_continual_pretraining · e2_sft · e3_dpo · eval
├── configs/default.yaml # единый конфиг (пути + гиперпараметры)
├── Dockerfile # CUDA 12.6 + сборка fast_align + JupyterLab
├── requirements.txt
└── .env.example
- cloud.ru ML Space, конфигурация
a100.1gpu(1× A100 80GB) — целевая среда запуска; локально подойдёт любая машина с A100 80GB, Docker и поддержкой CUDA - Веса базовой модели и публичные датасеты, скачанные файлами (в NFS на cloud.ru или локально)
- MLflow tracking server
Образ ставит рабочий стек и поднимает JupyterLab:
docker build -t mistral-lrd .Для запуска в cloud.ru образ публикуется в реестр воркспейса (суффикс -a100 — для региона с A100):
docker tag mistral-lrd cr.ai.cloud.ru/<workspace>/mistral-lrd-a100:0.1
docker login cr.ai.cloud.ru
docker push cr.ai.cloud.ru/<workspace>/mistral-lrd-a100:0.1python scripts/data_prep/build_culturax.py \
--input /data/raw/culturax_tt \
--out-tokenizer /data/tatar/mono_tokenizer.jsonl \
--out-cpt /data/tatar/mono_cpt.jsonl \
--out-fertility /data/tatar/eval/fertility.jsonl
python scripts/data_prep/build_parallel.py --input /data/raw/tt_ru_mt --out /data/tatar/ru_tt_parallel.jsonl
python scripts/data_prep/build_flores.py --input /data/raw/flores_tat_Cyrl.parquet --out /data/tatar/eval/ppl.jsonlИнструкционные диалоги, IFEval-tt, preference-пары и LCWR-промпты подаются готовыми JSONL в схемах
выше. Полный контракт схем — в scripts/data_prep/README.md.
cp .env.example .envВ configs/default.yaml (или через env MLRD_PATHS__*) указать пути к данным, весам базовой модели
и fast_align; выставить MLFLOW_TRACKING_URI. Required-пути без значения вызывают ошибку при
загрузке конфига (fail-fast).
На cloud.ru: запустить Jupyter Server на конфигурации a100.1gpu из опубликованного образа (данные
примонтированы из NFS, задан MLFLOW_TRACKING_URI). Локально тот же образ поднимается через Docker:
docker run --gpus all -p 8888:8888 -v /data:/data -v /work:/work \
-e MLFLOW_TRACKING_URI=$MLFLOW_TRACKING_URI mistral-lrdНоутбуки запускаются по порядку — по одному на этап адаптации:
e1_vocabulary_transfer.ipynb→ M1-init (с префлайтом fast_align и детекции префиксов)e1_continual_pretraining.ipynb→ M1e2_sft.ipynb→ M2e3_dpo.ipynb→ M3eval.ipynb→ 6 метрик по этапам baseline / M1 / M2 / M3 в MLflow
Метрика LCWR замеряется в три шага: export_lcwr_pairs генерирует ответы M3 и M2 → носители
проставляют win → run_eval(cfg, "m3", metrics=["lcwr"]) строит length-controlled win rate по
разметке.