Spaces:
Running on Zero
Running on Zero
Commit ·
e18df89
1
Parent(s): f84978d
Pin Rio v4 as standalone Modal module
Browse filesReplace the v4 import wrapper with a standalone Modal module whose variant constants are pinned to v4. The wrapper approach let Modal execute the shared v3 module remotely, so the first attempt accepted v3-only teacher categories and wrote a partial teacher file to the v3 data volume.\n\nAdd a separate one-shot restore app that repopulates the v3 data volume from the published Hugging Face v3 dataset package. The runlog records the stopped app, the root cause, and the repair path so the failed attempt is auditable.
finetune/reports/minicpm5_vivamais_text_runlog.md
CHANGED
|
@@ -2495,3 +2495,32 @@ Because v4 intentionally drops the generic/WhatsApp bucket, the v4 acceptance
|
|
| 2495 |
gate keeps the dashboard requirements strict (average improvement, unknown
|
| 2496 |
failures no worse, leakage failures no worse) and treats generic PT-BR as a
|
| 2497 |
minimum sanity check instead of requiring an overall generic win.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2495 |
gate keeps the dashboard requirements strict (average improvement, unknown
|
| 2496 |
failures no worse, leakage failures no worse) and treats generic PT-BR as a
|
| 2497 |
minimum sanity check instead of requiring an overall generic win.
|
| 2498 |
+
|
| 2499 |
+
### Aborted v4 wrapper attempt
|
| 2500 |
+
|
| 2501 |
+
```text
|
| 2502 |
+
run: https://modal.com/apps/marinaleitecabrera/main/ap-5Z9IbspvamaWKsCF1C6ePt
|
| 2503 |
+
status: stopped manually
|
| 2504 |
+
reason: the import-wrapper v4 launcher resolved the shared module as v3 remotely
|
| 2505 |
+
```
|
| 2506 |
+
|
| 2507 |
+
The first v4 attempt was stopped during teacher generation after the logs showed
|
| 2508 |
+
v3-only categories such as `customer_service`, `whatsapp_style`, and `open_chat`
|
| 2509 |
+
being accepted. The cause was the small v4 wrapper setting `VIVAMAIS_TEXT_VARIANT`
|
| 2510 |
+
only in the local launcher module; Modal mounted and executed the shared v3
|
| 2511 |
+
module remotely, so the constants fell back to v3 and wrote a partial teacher
|
| 2512 |
+
file to the v3 data volume.
|
| 2513 |
+
|
| 2514 |
+
Corrective action:
|
| 2515 |
+
|
| 2516 |
+
- Replace the v4 wrapper with a standalone v4 module whose constants are pinned
|
| 2517 |
+
to `TEXT_VARIANT = "v4"` and `IS_V4 = True`.
|
| 2518 |
+
- Restore the v3 data volume from the already-published HF dataset using a
|
| 2519 |
+
separate one-shot app:
|
| 2520 |
+
|
| 2521 |
+
```text
|
| 2522 |
+
script: modal/restore_vivamais_v3_dataset_volume.py
|
| 2523 |
+
app: vivamais-v3-dataset-volume-restore
|
| 2524 |
+
source dataset: marinarosa/minicpm5-vivamais-text-sft-v3
|
| 2525 |
+
target volume: minicpm5-vivamais-v3-rio-data
|
| 2526 |
+
```
|
modal/minicpm5_vivamais_v4_rio_pipeline.py
CHANGED
|
@@ -1,9 +1,1700 @@
|
|
| 1 |
-
"""Ephemeral Modal
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
import os
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ephemeral Modal pipeline for Viva Mais MiniCPM5 text v4 with Rio distillation."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import json
|
| 6 |
import os
|
| 7 |
+
import re
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
|
| 11 |
+
import modal
|
| 12 |
|
| 13 |
+
TEXT_VARIANT = "v4"
|
| 14 |
+
IS_V4 = True
|
| 15 |
+
|
| 16 |
+
APP_NAME = "minicpm5-vivamais-v4-rio-safety" if IS_V4 else "minicpm5-vivamais-v3-rio"
|
| 17 |
+
DATA_VOLUME_NAME = (
|
| 18 |
+
"minicpm5-vivamais-v4-rio-safety-data"
|
| 19 |
+
if IS_V4
|
| 20 |
+
else "minicpm5-vivamais-v3-rio-data"
|
| 21 |
+
)
|
| 22 |
+
RESULTS_VOLUME_NAME = (
|
| 23 |
+
"minicpm5-vivamais-v4-rio-safety-results"
|
| 24 |
+
if IS_V4
|
| 25 |
+
else "minicpm5-vivamais-v3-rio-results"
|
| 26 |
+
)
|
| 27 |
+
V1_MODEL = "marinarosa/minicpm5-1b-vivamais-v1"
|
| 28 |
+
V3_MODEL_REPO = (
|
| 29 |
+
"marinarosa/minicpm5-1b-vivamais-v4"
|
| 30 |
+
if IS_V4
|
| 31 |
+
else "marinarosa/minicpm5-1b-vivamais-v3"
|
| 32 |
+
)
|
| 33 |
+
V3_GGUF_REPO = (
|
| 34 |
+
"marinarosa/minicpm5-1b-vivamais-v4-GGUF"
|
| 35 |
+
if IS_V4
|
| 36 |
+
else "marinarosa/minicpm5-1b-vivamais-v3-GGUF"
|
| 37 |
+
)
|
| 38 |
+
RIO_TEACHER_MODEL = "prefeitura-rio/Rio-3.1-Open-4B-Instruct"
|
| 39 |
+
TRAIN_REMOTE = (
|
| 40 |
+
"/data/minicpm5_v4_rio_sft.jsonl"
|
| 41 |
+
if IS_V4
|
| 42 |
+
else "/data/minicpm5_v3_rio_sft.jsonl"
|
| 43 |
+
)
|
| 44 |
+
EVAL_REMOTE = "/data/vivamais_qa_eval.jsonl"
|
| 45 |
+
TEACHER_REMOTE = (
|
| 46 |
+
"/data/teacher_distill_v4_rio.jsonl"
|
| 47 |
+
if IS_V4
|
| 48 |
+
else "/data/teacher_distill_v3_rio.jsonl"
|
| 49 |
+
)
|
| 50 |
+
LORA_DIR = "/data/text-v4-rio-lora-out" if IS_V4 else "/data/text-v3-rio-lora-out"
|
| 51 |
+
MERGED_DIR = "/data/text-v4-rio-merged-out" if IS_V4 else "/data/text-v3-rio-merged-out"
|
| 52 |
+
GGUF_DIR = "/data/text-v4-rio-gguf" if IS_V4 else "/data/text-v3-rio-gguf"
|
| 53 |
+
DEFAULT_CONTEXT_LENGTH = 8192
|
| 54 |
+
NO_THINKING_RESPONSE_PREFIX = "<|im_start|>assistant\n<think>\n\n</think>\n\n"
|
| 55 |
+
DEFAULT_V3_ROWS = 4_000 if IS_V4 else 3_500
|
| 56 |
+
DEFAULT_TEACHER_ROWS = 80 if IS_V4 else 140
|
| 57 |
+
DEFAULT_TEACHER_SEED_LIMIT = 0
|
| 58 |
+
DEFAULT_LEARNING_RATE = 1.0e-5 if IS_V4 else 1.2e-5
|
| 59 |
+
DEFAULT_EPOCHS = 2.0 if IS_V4 else 1.2
|
| 60 |
+
V3_MIN_DASHBOARD_DELTA = 0.03
|
| 61 |
+
V3_MIN_GENERIC_SCORE = 0.55 if IS_V4 else 0.70
|
| 62 |
+
DEFAULT_PUBLIC_WEIGHT = 0.0
|
| 63 |
+
DEFAULT_GENERIC_WEIGHT = 0.0 if IS_V4 else 0.35
|
| 64 |
+
DEFAULT_DOMAIN_WEIGHT = 0.38 if IS_V4 else 0.25
|
| 65 |
+
DEFAULT_NEGATIVE_WEIGHT = 0.60 if IS_V4 else 0.36
|
| 66 |
+
DEFAULT_DISTILL_WEIGHT = 0.02 if IS_V4 else 0.04
|
| 67 |
+
DEFAULT_RUN_NAME = "v4_rio_001" if IS_V4 else "v3_rio_001"
|
| 68 |
+
TRAIN_GPU = "A100" if IS_V4 else "A10G"
|
| 69 |
+
GENERATION_GPU = "A100" if IS_V4 else "A10G"
|
| 70 |
+
V4_TEACHER_CATEGORIES = {
|
| 71 |
+
"entity_isolation",
|
| 72 |
+
"grounded_qa",
|
| 73 |
+
"grounded_refusal",
|
| 74 |
+
"grounded_unknown",
|
| 75 |
+
"payment_math",
|
| 76 |
+
"summary_rewrite",
|
| 77 |
+
"summarization",
|
| 78 |
+
"travel_qa",
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
app = modal.App(APP_NAME)
|
| 82 |
+
data_volume = modal.Volume.from_name(DATA_VOLUME_NAME, create_if_missing=True)
|
| 83 |
+
results_volume = modal.Volume.from_name(RESULTS_VOLUME_NAME, create_if_missing=True)
|
| 84 |
+
|
| 85 |
+
image = (
|
| 86 |
+
modal.Image.debian_slim(python_version="3.11")
|
| 87 |
+
.apt_install("git", "cmake", "build-essential", "curl", "libcurl4-openssl-dev", "libssl-dev")
|
| 88 |
+
.pip_install(
|
| 89 |
+
"unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git",
|
| 90 |
+
"trl",
|
| 91 |
+
"transformers>=4.51.0",
|
| 92 |
+
"datasets",
|
| 93 |
+
"accelerate",
|
| 94 |
+
"peft",
|
| 95 |
+
"huggingface_hub",
|
| 96 |
+
"sentencepiece",
|
| 97 |
+
"protobuf",
|
| 98 |
+
"bitsandbytes",
|
| 99 |
+
)
|
| 100 |
+
.add_local_file(
|
| 101 |
+
local_path="finetune/build_vivamais_qa_eval.py",
|
| 102 |
+
remote_path="/opt/vivamais/finetune/build_vivamais_qa_eval.py",
|
| 103 |
+
)
|
| 104 |
+
.add_local_file(
|
| 105 |
+
local_path="finetune/prepare_minicpm5_text_sft.py",
|
| 106 |
+
remote_path="/opt/vivamais/finetune/prepare_minicpm5_text_sft.py",
|
| 107 |
+
)
|
| 108 |
+
.add_local_file(
|
| 109 |
+
local_path="finetune/prepare_minicpm5_v2_distill_sft.py",
|
| 110 |
+
remote_path="/opt/vivamais/finetune/prepare_minicpm5_v2_distill_sft.py",
|
| 111 |
+
)
|
| 112 |
+
.add_local_file(
|
| 113 |
+
local_path="finetune/redact.py",
|
| 114 |
+
remote_path="/opt/vivamais/finetune/redact.py",
|
| 115 |
+
)
|
| 116 |
+
.add_local_file(
|
| 117 |
+
local_path="evals/vivamais_qa_scoring.py",
|
| 118 |
+
remote_path="/opt/vivamais/evals/vivamais_qa_scoring.py",
|
| 119 |
+
)
|
| 120 |
+
.add_local_file(
|
| 121 |
+
local_path="evals/ptbr_conversation_eval.py",
|
| 122 |
+
remote_path="/opt/vivamais/evals/ptbr_conversation_eval.py",
|
| 123 |
+
)
|
| 124 |
+
.add_local_dir(local_path="src", remote_path="/opt/vivamais/src")
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
inspect_image = (
|
| 128 |
+
modal.Image.debian_slim(python_version="3.11")
|
| 129 |
+
.pip_install("pytest")
|
| 130 |
+
.add_local_file(
|
| 131 |
+
local_path="finetune/build_vivamais_qa_eval.py",
|
| 132 |
+
remote_path="/opt/vivamais/finetune/build_vivamais_qa_eval.py",
|
| 133 |
+
)
|
| 134 |
+
.add_local_file(
|
| 135 |
+
local_path="evals/vivamais_qa_scoring.py",
|
| 136 |
+
remote_path="/opt/vivamais/evals/vivamais_qa_scoring.py",
|
| 137 |
+
)
|
| 138 |
+
.add_local_file(
|
| 139 |
+
local_path="evals/ptbr_conversation_eval.py",
|
| 140 |
+
remote_path="/opt/vivamais/evals/ptbr_conversation_eval.py",
|
| 141 |
+
)
|
| 142 |
+
.add_local_file(
|
| 143 |
+
local_path="tests/unit/finetune/test_build_vivamais_qa_eval.py",
|
| 144 |
+
remote_path="/opt/vivamais/tests/unit/finetune/test_build_vivamais_qa_eval.py",
|
| 145 |
+
)
|
| 146 |
+
.add_local_file(
|
| 147 |
+
local_path="tests/unit/evals/test_vivamais_qa_scoring.py",
|
| 148 |
+
remote_path="/opt/vivamais/tests/unit/evals/test_vivamais_qa_scoring.py",
|
| 149 |
+
)
|
| 150 |
+
.add_local_file(
|
| 151 |
+
local_path="tests/unit/evals/test_ptbr_conversation_eval.py",
|
| 152 |
+
remote_path="/opt/vivamais/tests/unit/evals/test_ptbr_conversation_eval.py",
|
| 153 |
+
)
|
| 154 |
+
.add_local_dir(local_path="src", remote_path="/opt/vivamais/src")
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _ensure_paths() -> None:
|
| 159 |
+
import sys
|
| 160 |
+
|
| 161 |
+
for path in (
|
| 162 |
+
"/opt/vivamais",
|
| 163 |
+
"/opt/vivamais/src",
|
| 164 |
+
"/opt/vivamais/finetune",
|
| 165 |
+
"/opt/vivamais/evals",
|
| 166 |
+
):
|
| 167 |
+
if path not in sys.path:
|
| 168 |
+
sys.path.insert(0, path)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _load_jsonl(path: Path) -> list[dict[str, Any]]:
|
| 172 |
+
rows: list[dict[str, Any]] = []
|
| 173 |
+
if not path.is_file():
|
| 174 |
+
return rows
|
| 175 |
+
with path.open(encoding="utf-8") as handle:
|
| 176 |
+
for line in handle:
|
| 177 |
+
if line.strip():
|
| 178 |
+
row = json.loads(line)
|
| 179 |
+
if isinstance(row, dict):
|
| 180 |
+
rows.append(row)
|
| 181 |
+
return rows
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _write_jsonl(path: Path, row: dict[str, Any]) -> None:
|
| 185 |
+
with path.open("a", encoding="utf-8") as handle:
|
| 186 |
+
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _preview(text: str, limit: int = 500) -> str:
|
| 190 |
+
text = text.strip()
|
| 191 |
+
if len(text) <= limit:
|
| 192 |
+
return text
|
| 193 |
+
return f"{text[:limit].rstrip()}..."
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
ACCENT_REPLACEMENTS = (
|
| 197 |
+
(r"\b[Nn]ao\b", "não"),
|
| 198 |
+
(r"\b[Nn]a\b", "na"),
|
| 199 |
+
(r"\binformacao\b", "informação"),
|
| 200 |
+
(r"\binformacoes\b", "informações"),
|
| 201 |
+
(r"\borcamento\b", "orçamento"),
|
| 202 |
+
(r"\btransacao\b", "transação"),
|
| 203 |
+
(r"\btransacoes\b", "transações"),
|
| 204 |
+
(r"\bcartao\b", "cartão"),
|
| 205 |
+
(r"\bcodigo\b", "código"),
|
| 206 |
+
(r"\bportao\b", "portão"),
|
| 207 |
+
(r"\baereo\b", "aéreo"),
|
| 208 |
+
(r"\baerea\b", "aérea"),
|
| 209 |
+
(r"\bate\b", "até"),
|
| 210 |
+
(r"\bvoce\b", "você"),
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
ACCENTLESS_MARKERS = (
|
| 214 |
+
" nao",
|
| 215 |
+
"nao ",
|
| 216 |
+
"informacao",
|
| 217 |
+
"informacoes",
|
| 218 |
+
"orcamento",
|
| 219 |
+
"transacao",
|
| 220 |
+
"transacoes",
|
| 221 |
+
"cartao",
|
| 222 |
+
"codigo",
|
| 223 |
+
"portao",
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
INSTRUCTION_ECHO_PREFIXES = (
|
| 227 |
+
"avise ",
|
| 228 |
+
"avise que",
|
| 229 |
+
"confirme ",
|
| 230 |
+
"diga ",
|
| 231 |
+
"diga que",
|
| 232 |
+
"escreva ",
|
| 233 |
+
"explique ",
|
| 234 |
+
"informe ",
|
| 235 |
+
"peça ",
|
| 236 |
+
"peca ",
|
| 237 |
+
"pergunte ",
|
| 238 |
+
"responda ",
|
| 239 |
+
"resposta:",
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def _normalize_ptbr_accents(text: str) -> str:
|
| 244 |
+
normalized = text
|
| 245 |
+
for pattern, replacement in ACCENT_REPLACEMENTS:
|
| 246 |
+
normalized = re.sub(pattern, replacement, normalized, flags=re.IGNORECASE)
|
| 247 |
+
return normalized
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def _configure_tokenizer_for_generation(tokenizer: Any, model: Any | None = None) -> None:
|
| 251 |
+
if getattr(tokenizer, "pad_token_id", None) is None:
|
| 252 |
+
tokenizer.pad_token = getattr(tokenizer, "eos_token", None)
|
| 253 |
+
pad_token_id = getattr(tokenizer, "pad_token_id", None)
|
| 254 |
+
if model is None or pad_token_id is None:
|
| 255 |
+
return
|
| 256 |
+
if getattr(model, "config", None) is not None:
|
| 257 |
+
model.config.pad_token_id = pad_token_id
|
| 258 |
+
if getattr(model, "generation_config", None) is not None:
|
| 259 |
+
model.generation_config.pad_token_id = pad_token_id
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
@app.function(image=inspect_image, timeout=10 * 60, cpu=2, memory=4096)
|
| 263 |
+
def inspect_eval_suites() -> dict[str, Any]:
|
| 264 |
+
from collections import Counter
|
| 265 |
+
|
| 266 |
+
_ensure_paths()
|
| 267 |
+
from build_vivamais_qa_eval import build_eval_cases
|
| 268 |
+
from ptbr_conversation_eval import GENERIC_EVAL_CASES
|
| 269 |
+
from vivamais_qa_scoring import score_case
|
| 270 |
+
|
| 271 |
+
dashboard_cases = build_eval_cases()
|
| 272 |
+
dashboard_ids = [str(case["id"]) for case in dashboard_cases]
|
| 273 |
+
generic_ids = [str(case["id"]) for case in GENERIC_EVAL_CASES]
|
| 274 |
+
unknown_probe = score_case(
|
| 275 |
+
{
|
| 276 |
+
"id": "unknown_probe",
|
| 277 |
+
"category": "not_enough_information",
|
| 278 |
+
"answer_type": "unknown",
|
| 279 |
+
"expected_answer": "Não há informação suficiente no contexto.",
|
| 280 |
+
"expected_contains": [],
|
| 281 |
+
"forbidden": ["[HOTEL_1]"],
|
| 282 |
+
},
|
| 283 |
+
"Não consta hospedagem no contexto informado.",
|
| 284 |
+
)
|
| 285 |
+
summary = {
|
| 286 |
+
"dashboard_cases": len(dashboard_cases),
|
| 287 |
+
"dashboard_categories": dict(Counter(str(case["category"]) for case in dashboard_cases)),
|
| 288 |
+
"dashboard_long_context_cases": sum(1 for case in dashboard_cases if case["long_context"]),
|
| 289 |
+
"dashboard_duplicate_ids": len(dashboard_ids) - len(set(dashboard_ids)),
|
| 290 |
+
"generic_cases": len(GENERIC_EVAL_CASES),
|
| 291 |
+
"generic_categories": dict(Counter(str(case["category"]) for case in GENERIC_EVAL_CASES)),
|
| 292 |
+
"generic_duplicate_ids": len(generic_ids) - len(set(generic_ids)),
|
| 293 |
+
"unknown_probe_passed": bool(unknown_probe["passed"]),
|
| 294 |
+
}
|
| 295 |
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
| 296 |
+
return summary
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
@app.function(image=inspect_image, timeout=10 * 60, cpu=2, memory=4096)
|
| 300 |
+
def run_eval_unit_subset() -> dict[str, Any]:
|
| 301 |
+
import os
|
| 302 |
+
import subprocess
|
| 303 |
+
|
| 304 |
+
env = os.environ.copy()
|
| 305 |
+
env["PYTHONPATH"] = "/opt/vivamais:/opt/vivamais/src:/opt/vivamais/evals"
|
| 306 |
+
command = [
|
| 307 |
+
"python",
|
| 308 |
+
"-m",
|
| 309 |
+
"pytest",
|
| 310 |
+
"/opt/vivamais/tests/unit/finetune/test_build_vivamais_qa_eval.py",
|
| 311 |
+
"/opt/vivamais/tests/unit/evals/test_vivamais_qa_scoring.py",
|
| 312 |
+
"/opt/vivamais/tests/unit/evals/test_ptbr_conversation_eval.py",
|
| 313 |
+
]
|
| 314 |
+
completed = subprocess.run(command, capture_output=True, text=True, env=env, check=False)
|
| 315 |
+
if completed.stdout:
|
| 316 |
+
print(completed.stdout)
|
| 317 |
+
if completed.stderr:
|
| 318 |
+
print(completed.stderr)
|
| 319 |
+
if completed.returncode != 0:
|
| 320 |
+
raise RuntimeError(f"Eval unit subset failed with exit code {completed.returncode}")
|
| 321 |
+
return {"command": command, "returncode": completed.returncode}
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
CURATED_TEACHER_PROMPTS = (
|
| 325 |
+
(
|
| 326 |
+
"open_chat",
|
| 327 |
+
"Escreva uma legenda curta e natural para Instagram sobre uma viagem em família.",
|
| 328 |
+
),
|
| 329 |
+
(
|
| 330 |
+
"customer_service",
|
| 331 |
+
(
|
| 332 |
+
"Responda com educação: o cliente achou o pacote caro e pediu uma "
|
| 333 |
+
"alternativa mais barata."
|
| 334 |
+
),
|
| 335 |
+
),
|
| 336 |
+
(
|
| 337 |
+
"travel_qa",
|
| 338 |
+
"Explique a diferença entre bagagem de mão, item pessoal e bagagem despachada.",
|
| 339 |
+
),
|
| 340 |
+
(
|
| 341 |
+
"travel_qa",
|
| 342 |
+
"Quando vale a pena contratar seguro viagem? Responda de forma simples.",
|
| 343 |
+
),
|
| 344 |
+
(
|
| 345 |
+
"summarization",
|
| 346 |
+
(
|
| 347 |
+
"Resuma em duas frases: cliente quer viajar em julho, prefere praia, "
|
| 348 |
+
"tem duas crianças e quer parcelar."
|
| 349 |
+
),
|
| 350 |
+
),
|
| 351 |
+
(
|
| 352 |
+
"grounded_qa",
|
| 353 |
+
"Contexto: Total R$3.000,00. Pago R$1.250,00. Pergunta: Quanto falta pagar?",
|
| 354 |
+
),
|
| 355 |
+
(
|
| 356 |
+
"grounded_qa",
|
| 357 |
+
"Contexto: Reserva LA 3456 em 12/07 às 08:30, assento 12A. Pergunta: Qual é o assento?",
|
| 358 |
+
),
|
| 359 |
+
(
|
| 360 |
+
"grounded_unknown",
|
| 361 |
+
(
|
| 362 |
+
"Contexto: A reserva informa voo LA 3456 e pagamento de R$1.250,00. "
|
| 363 |
+
"Pergunta: Qual é o hotel?"
|
| 364 |
+
),
|
| 365 |
+
),
|
| 366 |
+
(
|
| 367 |
+
"whatsapp_style",
|
| 368 |
+
"Transforme em uma mensagem curta de WhatsApp: precisamos do RG para emitir os bilhetes.",
|
| 369 |
+
),
|
| 370 |
+
(
|
| 371 |
+
"informal_ptbr",
|
| 372 |
+
"O cliente escreveu 'tá caro demais'. Responda de forma profissional e natural.",
|
| 373 |
+
),
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def _synthetic_vivamais_teacher_prompts() -> list[tuple[str, str]]:
|
| 378 |
+
prompts: list[tuple[str, str]] = []
|
| 379 |
+
balances = (
|
| 380 |
+
("R$3.000,00", "R$1.250,00"),
|
| 381 |
+
("R$4.800,00", "R$1.950,00"),
|
| 382 |
+
("R$2.200,00", "R$2.200,00"),
|
| 383 |
+
("R$5.400,00", "R$2.100,00"),
|
| 384 |
+
("R$980,00", "R$300,00"),
|
| 385 |
+
("R$3.600,00", "R$900,00"),
|
| 386 |
+
("R$1.870,00", "R$870,00"),
|
| 387 |
+
("R$6.250,00", "R$2.500,00"),
|
| 388 |
+
)
|
| 389 |
+
for total, paid in balances:
|
| 390 |
+
prompts.append(
|
| 391 |
+
(
|
| 392 |
+
f"Contexto: total {total}; pago {paid}. Pergunta: quanto falta pagar?",
|
| 393 |
+
"payment_math",
|
| 394 |
+
)
|
| 395 |
+
)
|
| 396 |
+
prompts.append(
|
| 397 |
+
(
|
| 398 |
+
f"Escreva uma resposta curta de WhatsApp avisando que já entrou {paid} "
|
| 399 |
+
f"de um pacote de {total}.",
|
| 400 |
+
"whatsapp_payment",
|
| 401 |
+
)
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
missing_fields = (
|
| 405 |
+
("hotel", "voo LA 3456 e pagamento de R$1.250,00"),
|
| 406 |
+
("assento", "localizador ABC123 e data 12/07"),
|
| 407 |
+
("bagagem despachada", "valor R$2.700,00 e prazo até sexta"),
|
| 408 |
+
("validade do orçamento", "destino Recife e total R$4.800,00"),
|
| 409 |
+
("pagamento", "cliente pediu cotação, mas não enviou comprovante"),
|
| 410 |
+
("documento do acompanhante", "passageiro titular enviou RG"),
|
| 411 |
+
)
|
| 412 |
+
for field, context in missing_fields:
|
| 413 |
+
prompts.append(
|
| 414 |
+
(
|
| 415 |
+
f"Contexto: {context}. Pergunta: qual é {field}? Responda sem inventar.",
|
| 416 |
+
"grounded_refusal",
|
| 417 |
+
)
|
| 418 |
+
)
|
| 419 |
+
prompts.append(
|
| 420 |
+
(
|
| 421 |
+
f"Reescreva de forma natural: não consta no contexto a informação sobre {field}.",
|
| 422 |
+
"grounded_refusal",
|
| 423 |
+
)
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
customer_prompts = (
|
| 427 |
+
"O cliente disse: 'tá caro demais'. Responda de forma curta e profissional.",
|
| 428 |
+
"O cliente pediu uma opção mais barata. Responda com empatia e proponha revisar datas.",
|
| 429 |
+
"Peça o RG do acompanhante de forma natural para WhatsApp.",
|
| 430 |
+
"Avise que a companhia ainda não confirmou a emissão e que você vai acompanhar.",
|
| 431 |
+
"Ofereça seguro viagem de forma consultiva, sem dizer que é obrigatório.",
|
| 432 |
+
"Peça flexibilidade de datas em uma frase curta.",
|
| 433 |
+
"Faça follow-up perguntando se o cliente conseguiu olhar a proposta.",
|
| 434 |
+
"Avise que a tarifa pode mudar sem soar alarmista.",
|
| 435 |
+
"Peça desculpas pelo atraso na resposta e retome o atendimento.",
|
| 436 |
+
"Pergunte destino, datas e quantidade de passageiros para iniciar uma cotação.",
|
| 437 |
+
)
|
| 438 |
+
prompts.extend(
|
| 439 |
+
(
|
| 440 |
+
f"Responda somente com a mensagem final para WhatsApp: {prompt}",
|
| 441 |
+
"customer_service",
|
| 442 |
+
)
|
| 443 |
+
for prompt in customer_prompts
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
travel_prompts = (
|
| 447 |
+
"Explique em poucas frases a diferença entre bagagem de mão e bagagem despachada.",
|
| 448 |
+
"Quando normalmente abre o check-in online de voo doméstico?",
|
| 449 |
+
"O que significa conexão em um voo?",
|
| 450 |
+
"Para que serve o localizador da reserva?",
|
| 451 |
+
"Qual é a diferença entre check-in e check-out no hotel?",
|
| 452 |
+
"O que o seguro viagem costuma cobrir? Responda de forma simples.",
|
| 453 |
+
"Como explicar franquia de bagagem para um cliente leigo?",
|
| 454 |
+
"O que o cliente precisa conferir antes da emissão da passagem?",
|
| 455 |
+
"Explique por que tarifa aérea pode mudar antes da emissão.",
|
| 456 |
+
"Diga quando faz sentido reservar hotel próximo ao aeroporto.",
|
| 457 |
+
)
|
| 458 |
+
prompts.extend(
|
| 459 |
+
(f"Responda em até duas frases, sem lista: {prompt}", "travel_qa")
|
| 460 |
+
for prompt in travel_prompts
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
entity_cases = (
|
| 464 |
+
(
|
| 465 |
+
"[CLIENTE_A] tem voo AD 2210, localizador AAA111 e embarque 14/09.",
|
| 466 |
+
"[CLIENTE_B] tem voo LA 9090, localizador BBB222 e embarque 20/10.",
|
| 467 |
+
"[CLIENTE_A]",
|
| 468 |
+
"qual é o localizador?",
|
| 469 |
+
),
|
| 470 |
+
(
|
| 471 |
+
"[CLIENTE_A] pagou R$1.200,00 e ainda falta R$800,00.",
|
| 472 |
+
"[CLIENTE_B] pagou R$900,00 e ainda falta R$1.100,00.",
|
| 473 |
+
"[CLIENTE_B]",
|
| 474 |
+
"quanto falta pagar?",
|
| 475 |
+
),
|
| 476 |
+
(
|
| 477 |
+
"[CLIENTE_A] já enviou RG e comprovante.",
|
| 478 |
+
"[CLIENTE_B] enviou apenas o RG.",
|
| 479 |
+
"[CLIENTE_B]",
|
| 480 |
+
"qual documento ainda falta?",
|
| 481 |
+
),
|
| 482 |
+
(
|
| 483 |
+
"[CLIENTE_A] viaja para Recife em 08/08 com hotel confirmado.",
|
| 484 |
+
"[CLIENTE_B] viaja para Salvador em 19/08 e aguarda hotel.",
|
| 485 |
+
"[CLIENTE_A]",
|
| 486 |
+
"qual é o destino?",
|
| 487 |
+
),
|
| 488 |
+
(
|
| 489 |
+
"[CLIENTE_A] tem assento 12A e bagagem despachada inclusa.",
|
| 490 |
+
"[CLIENTE_B] tem assento 18C e apenas bagagem de mão.",
|
| 491 |
+
"[CLIENTE_B]",
|
| 492 |
+
"qual é o assento?",
|
| 493 |
+
),
|
| 494 |
+
(
|
| 495 |
+
"[CLIENTE_A] recebeu orçamento válido até 18:00.",
|
| 496 |
+
"[CLIENTE_B] recebeu orçamento válido até 21:00.",
|
| 497 |
+
"[CLIENTE_A]",
|
| 498 |
+
"até que hora vale o orçamento?",
|
| 499 |
+
),
|
| 500 |
+
(
|
| 501 |
+
"[CLIENTE_A] pediu seguro viagem e traslado.",
|
| 502 |
+
"[CLIENTE_B] recusou seguro viagem e pediu só hospedagem.",
|
| 503 |
+
"[CLIENTE_A]",
|
| 504 |
+
"quais extras foram solicitados?",
|
| 505 |
+
),
|
| 506 |
+
(
|
| 507 |
+
"[CLIENTE_A] tem reserva no [HOTEL_1].",
|
| 508 |
+
"[CLIENTE_B] tem reserva no [HOTEL_2].",
|
| 509 |
+
"[CLIENTE_B]",
|
| 510 |
+
"qual hotel está reservado?",
|
| 511 |
+
),
|
| 512 |
+
(
|
| 513 |
+
"[CLIENTE_A] parcelou em 6 vezes sem entrada.",
|
| 514 |
+
"[CLIENTE_B] deu entrada de R$500,00 e parcelou o restante.",
|
| 515 |
+
"[CLIENTE_B]",
|
| 516 |
+
"qual foi a entrada?",
|
| 517 |
+
),
|
| 518 |
+
(
|
| 519 |
+
"[CLIENTE_A] está em cotação.",
|
| 520 |
+
"[CLIENTE_B] está aguardando emissão.",
|
| 521 |
+
"[CLIENTE_A]",
|
| 522 |
+
"em que etapa está o atendimento?",
|
| 523 |
+
),
|
| 524 |
+
(
|
| 525 |
+
"[CLIENTE_A] vai embarcar no aeroporto SDU.",
|
| 526 |
+
"[CLIENTE_B] vai embarcar no aeroporto GIG.",
|
| 527 |
+
"[CLIENTE_B]",
|
| 528 |
+
"qual é o aeroporto de embarque?",
|
| 529 |
+
),
|
| 530 |
+
(
|
| 531 |
+
"[CLIENTE_A] confirmou duas crianças.",
|
| 532 |
+
"[CLIENTE_B] confirmou um adulto e uma criança.",
|
| 533 |
+
"[CLIENTE_A]",
|
| 534 |
+
"quantas crianças foram confirmadas?",
|
| 535 |
+
),
|
| 536 |
+
(
|
| 537 |
+
"[CLIENTE_A] pediu quarto triplo.",
|
| 538 |
+
"[CLIENTE_B] pediu quarto duplo.",
|
| 539 |
+
"[CLIENTE_A]",
|
| 540 |
+
"qual tipo de quarto foi pedido?",
|
| 541 |
+
),
|
| 542 |
+
(
|
| 543 |
+
"[CLIENTE_A] precisa enviar CPF.",
|
| 544 |
+
"[CLIENTE_B] precisa enviar comprovante de residência.",
|
| 545 |
+
"[CLIENTE_B]",
|
| 546 |
+
"o que ainda precisa enviar?",
|
| 547 |
+
),
|
| 548 |
+
(
|
| 549 |
+
"[CLIENTE_A] vai retornar em 25/11.",
|
| 550 |
+
"[CLIENTE_B] vai retornar em 28/11.",
|
| 551 |
+
"[CLIENTE_A]",
|
| 552 |
+
"qual é a data de retorno?",
|
| 553 |
+
),
|
| 554 |
+
(
|
| 555 |
+
"[CLIENTE_A] aprovou a proposta de R$3.400,00.",
|
| 556 |
+
"[CLIENTE_B] ainda está comparando proposta de R$2.900,00.",
|
| 557 |
+
"[CLIENTE_A]",
|
| 558 |
+
"qual proposta foi aprovada?",
|
| 559 |
+
),
|
| 560 |
+
(
|
| 561 |
+
"[CLIENTE_A] solicitou nota fiscal.",
|
| 562 |
+
"[CLIENTE_B] solicitou recibo simples.",
|
| 563 |
+
"[CLIENTE_B]",
|
| 564 |
+
"qual comprovante foi solicitado?",
|
| 565 |
+
),
|
| 566 |
+
(
|
| 567 |
+
"[CLIENTE_A] vai levar bebê de colo.",
|
| 568 |
+
"[CLIENTE_B] vai viajar sem criança.",
|
| 569 |
+
"[CLIENTE_A]",
|
| 570 |
+
"há bebê de colo?",
|
| 571 |
+
),
|
| 572 |
+
)
|
| 573 |
+
for first, second, client, question in entity_cases:
|
| 574 |
+
prompts.append(
|
| 575 |
+
(
|
| 576 |
+
f"Contexto: {first} {second} Pergunta: sobre {client}, {question}",
|
| 577 |
+
"entity_isolation",
|
| 578 |
+
)
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
summary_cases = (
|
| 582 |
+
"cliente quer viajar em setembro, prefere praia, pediu parcelamento e falta RG",
|
| 583 |
+
"família de quatro pessoas quer Natal, precisa de hotel com café e traslado",
|
| 584 |
+
"casal pediu lua de mel em Gramado, orçamento alto e datas flexíveis",
|
| 585 |
+
"cliente aprovou voo, falta escolher hotel e enviar comprovante",
|
| 586 |
+
"passageira pediu remarcação, informou doença e aguarda regras da tarifa",
|
| 587 |
+
"grupo quer Buenos Aires, pediu bagagem despachada e pagamento no cartão",
|
| 588 |
+
"cliente quer economizar, aceita voo com conexão e hotel simples",
|
| 589 |
+
"cliente pediu pacote para Porto Seguro, duas crianças e saída em julho",
|
| 590 |
+
"cliente enviou comprovante, falta validar valor e confirmar emissão",
|
| 591 |
+
"cliente perguntou seguro viagem, bagagem e possibilidade de parcelar",
|
| 592 |
+
"cliente recusou primeira proposta e pediu opção com datas alternativas",
|
| 593 |
+
"cliente confirmou dados dos passageiros, mas falta documento do bebê",
|
| 594 |
+
"cliente quer viajar no feriado, prefere voo direto e hotel perto da praia",
|
| 595 |
+
"cliente pediu cancelamento, quer saber multa e prazo de reembolso",
|
| 596 |
+
"cliente recebeu orçamento, achou caro e pediu algo até R$3.000,00",
|
| 597 |
+
"cliente quer réveillon, não definiu destino e informou orçamento aberto",
|
| 598 |
+
"cliente pediu recibo, nota fiscal e confirmação do valor pago",
|
| 599 |
+
"cliente perguntou se o pacote inclui ingresso, traslado e café da manhã",
|
| 600 |
+
)
|
| 601 |
+
for details in summary_cases:
|
| 602 |
+
prompts.append(
|
| 603 |
+
(
|
| 604 |
+
f"Resuma em dois bullets para uma agente de viagens: {details}.",
|
| 605 |
+
"summary_rewrite",
|
| 606 |
+
)
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
service_prompts = (
|
| 610 |
+
"Responda ao cliente que pediu desconto, mantendo tom humano e sem prometer valor.",
|
| 611 |
+
"Avise que você vai conferir disponibilidade antes de confirmar a tarifa.",
|
| 612 |
+
"Peça os dados completos dos passageiros para emissão em uma mensagem curta.",
|
| 613 |
+
"Diga que a proposta foi atualizada com uma opção mais econômica.",
|
| 614 |
+
"Explique que a reserva só fica garantida depois da emissão.",
|
| 615 |
+
"Pergunte se o cliente prefere voo direto ou aceita conexão para economizar.",
|
| 616 |
+
"Avise que recebeu o comprovante e vai conferir com o financeiro.",
|
| 617 |
+
"Informe que falta apenas o documento do acompanhante para seguir.",
|
| 618 |
+
"Diga que encontrou uma opção com hotel melhor, mas valor mais alto.",
|
| 619 |
+
"Peça autorização final antes de emitir a passagem.",
|
| 620 |
+
"Responda a um cliente ansioso sem inventar prazo da companhia.",
|
| 621 |
+
"Avise que vai mandar duas opções: uma econômica e uma mais confortável.",
|
| 622 |
+
"Explique que parcelamento pode depender da forma de pagamento.",
|
| 623 |
+
"Confirme que os nomes precisam estar iguais aos documentos.",
|
| 624 |
+
"Peça desculpas pelo atraso e retome a conversa com a próxima ação.",
|
| 625 |
+
"Diga que a tarifa mudou e ofereça revisar datas para baixar o valor.",
|
| 626 |
+
"Pergunte se o cliente deseja incluir bagagem despachada.",
|
| 627 |
+
"Avise que o hotel solicitado não tem disponibilidade nas datas.",
|
| 628 |
+
"Confirme que vai salvar o orçamento e acompanhar a decisão do cliente.",
|
| 629 |
+
"Diga que precisa do comprovante legível para conciliar o pagamento.",
|
| 630 |
+
)
|
| 631 |
+
prompts.extend(
|
| 632 |
+
(
|
| 633 |
+
f"Responda somente com a mensagem final para WhatsApp: {prompt}",
|
| 634 |
+
"customer_service",
|
| 635 |
+
)
|
| 636 |
+
for prompt in service_prompts
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
workflow_prompts = (
|
| 640 |
+
"Escreva uma mensagem curta pedindo confirmação dos nomes antes da emissão.",
|
| 641 |
+
"Escreva uma mensagem curta avisando que a cotação vence hoje às 18:00.",
|
| 642 |
+
"Escreva uma resposta natural para cliente que perguntou se pode parcelar.",
|
| 643 |
+
"Escreva uma resposta curta dizendo que o comprovante ficou ilegível.",
|
| 644 |
+
"Escreva uma mensagem pedindo o CPF sem expor dados sensíveis.",
|
| 645 |
+
"Escreva uma resposta explicando que a tarifa depende de disponibilidade.",
|
| 646 |
+
"Escreva uma mensagem avisando que falta escolher o horário do voo.",
|
| 647 |
+
"Escreva uma resposta para cliente que pediu hotel com café da manhã.",
|
| 648 |
+
"Escreva uma mensagem confirmando recebimento do RG do passageiro.",
|
| 649 |
+
"Escreva uma resposta dizendo que você vai conferir regras de remarcação.",
|
| 650 |
+
"Escreva uma mensagem avisando que a opção mais barata tem conexão.",
|
| 651 |
+
"Escreva uma resposta explicando que voo direto ficou mais caro.",
|
| 652 |
+
"Escreva uma mensagem curta pedindo autorização para emitir.",
|
| 653 |
+
"Escreva uma resposta dizendo que o seguro viagem é opcional.",
|
| 654 |
+
"Escreva uma mensagem pedindo o comprovante de pagamento.",
|
| 655 |
+
"Escreva uma resposta para cliente que ainda não decidiu o destino.",
|
| 656 |
+
"Escreva uma mensagem oferecendo revisar as datas para reduzir o preço.",
|
| 657 |
+
"Escreva uma resposta dizendo que a hospedagem ainda não foi confirmada.",
|
| 658 |
+
"Escreva uma mensagem curta avisando que o pagamento foi localizado.",
|
| 659 |
+
"Escreva uma resposta explicando que crianças precisam de documento.",
|
| 660 |
+
"Escreva uma mensagem pedindo confirmação de ida e volta.",
|
| 661 |
+
"Escreva uma resposta dizendo que bagagem despachada muda o valor.",
|
| 662 |
+
"Escreva uma mensagem informando que a reserva está aguardando pagamento.",
|
| 663 |
+
"Escreva uma resposta educada para cliente que pediu retorno urgente.",
|
| 664 |
+
"Escreva uma mensagem dizendo que vai enviar duas alternativas de hotel.",
|
| 665 |
+
"Escreva uma resposta para cliente que pediu recibo após pagar.",
|
| 666 |
+
"Escreva uma mensagem curta pedindo telefone de contato atualizado.",
|
| 667 |
+
"Escreva uma resposta dizendo que a emissão depende da aprovação final.",
|
| 668 |
+
"Escreva uma mensagem avisando que o orçamento foi salvo no atendimento.",
|
| 669 |
+
"Escreva uma resposta para cliente que quer incluir mais um passageiro.",
|
| 670 |
+
)
|
| 671 |
+
prompts.extend(
|
| 672 |
+
(
|
| 673 |
+
f"Responda somente com a mensagem final para WhatsApp: {prompt}",
|
| 674 |
+
"whatsapp_style",
|
| 675 |
+
)
|
| 676 |
+
for prompt in workflow_prompts
|
| 677 |
+
)
|
| 678 |
+
|
| 679 |
+
grounded_cases = (
|
| 680 |
+
("Total R$3.400,00; pago R$1.000,00.", "quanto falta pagar?"),
|
| 681 |
+
("Total R$5.200,00; pago R$5.200,00.", "a viagem está quitada?"),
|
| 682 |
+
("Reserva informa voo AD 2210 e localizador VMA123.", "qual é o voo?"),
|
| 683 |
+
("Reserva informa voo AD 2210 e localizador VMA123.", "qual é o hotel?"),
|
| 684 |
+
("Cotação válida até 14/06 às 18:00.", "até quando vale?"),
|
| 685 |
+
("Cotação válida até 14/06 às 18:00.", "qual é a política de cancelamento?"),
|
| 686 |
+
("Passageira Ana enviou RG; Bruno ainda não enviou documento.", "quem está pendente?"),
|
| 687 |
+
("Pagamento recebido em PIX no valor de R$870,00.", "qual foi a forma de pagamento?"),
|
| 688 |
+
("Cliente pediu Recife, julho, duas crianças.", "qual é o destino?"),
|
| 689 |
+
("Cliente pediu Recife, julho, duas crianças.", "qual é o número do voo?"),
|
| 690 |
+
("Hotel confirmado com café da manhã, sem traslado.", "inclui traslado?"),
|
| 691 |
+
("Pacote inclui bagagem de mão e item pessoal.", "inclui bagagem despachada?"),
|
| 692 |
+
("Localizador ABC123, assento 12A, embarque 08:30.", "qual é o assento?"),
|
| 693 |
+
("Orçamento de R$2.980,00 em até 6 vezes.", "qual é o valor total?"),
|
| 694 |
+
("Cliente recusou seguro viagem e pediu só passagem.", "inclui seguro viagem?"),
|
| 695 |
+
("Cliente aprovou emissão após confirmar nomes.", "qual é a próxima etapa?"),
|
| 696 |
+
("Falta enviar CPF do acompanhante.", "qual documento está pendente?"),
|
| 697 |
+
("A tarifa mudou de R$2.400,00 para R$2.650,00.", "qual é o novo valor?"),
|
| 698 |
+
("Cliente quer voo direto, mas só há opção com conexão.", "há voo direto?"),
|
| 699 |
+
("Pagamento de entrada venceu ontem e não há comprovante.", "o pagamento foi confirmado?"),
|
| 700 |
+
)
|
| 701 |
+
for context, question in grounded_cases:
|
| 702 |
+
prompts.append(
|
| 703 |
+
(
|
| 704 |
+
f"Contexto: {context} Pergunta: {question} Responda sem inventar.",
|
| 705 |
+
"grounded_qa",
|
| 706 |
+
)
|
| 707 |
+
)
|
| 708 |
+
return prompts
|
| 709 |
+
|
| 710 |
+
|
| 711 |
+
def _collect_gigaverbo_prompts(*, limit: int, seed: int) -> list[tuple[str, str]]:
|
| 712 |
+
from datasets import load_dataset
|
| 713 |
+
|
| 714 |
+
subsets = ("general", "retrieval", "summarization", "rewriting", "system_prompts")
|
| 715 |
+
rng = __import__("random").Random(seed)
|
| 716 |
+
prompts: list[tuple[str, str]] = []
|
| 717 |
+
per_subset = max(1, limit // len(subsets))
|
| 718 |
+
for subset in subsets:
|
| 719 |
+
if len(prompts) >= limit:
|
| 720 |
+
break
|
| 721 |
+
try:
|
| 722 |
+
ds = load_dataset("Polygl0t/gigaverbo-v2-sft", subset, split="train", streaming=True)
|
| 723 |
+
except Exception as exc:
|
| 724 |
+
print(f"Skipping GigaVerbo subset {subset}: {exc}")
|
| 725 |
+
continue
|
| 726 |
+
seen = 0
|
| 727 |
+
for row in ds:
|
| 728 |
+
messages = row.get("messages")
|
| 729 |
+
if not isinstance(messages, list):
|
| 730 |
+
continue
|
| 731 |
+
users = [
|
| 732 |
+
str(message.get("content", "")).strip()
|
| 733 |
+
for message in messages
|
| 734 |
+
if isinstance(message, dict)
|
| 735 |
+
and message.get("role") == "user"
|
| 736 |
+
and str(message.get("content", "")).strip()
|
| 737 |
+
]
|
| 738 |
+
if not users:
|
| 739 |
+
continue
|
| 740 |
+
if rng.random() < 0.35:
|
| 741 |
+
prompts.append((users[-1], f"gigaverbo_{subset}"))
|
| 742 |
+
seen += 1
|
| 743 |
+
if seen >= per_subset or len(prompts) >= limit:
|
| 744 |
+
break
|
| 745 |
+
return prompts[:limit]
|
| 746 |
+
|
| 747 |
+
|
| 748 |
+
def _teacher_prompt_pool(*, gigaverbo_limit: int, seed: int) -> list[tuple[str, str]]:
|
| 749 |
+
prompts = [(prompt, category) for category, prompt in CURATED_TEACHER_PROMPTS]
|
| 750 |
+
prompts.extend(_synthetic_vivamais_teacher_prompts())
|
| 751 |
+
if gigaverbo_limit > 0:
|
| 752 |
+
prompts.extend(_collect_gigaverbo_prompts(limit=gigaverbo_limit, seed=seed))
|
| 753 |
+
if IS_V4:
|
| 754 |
+
prompts = [
|
| 755 |
+
(prompt, category)
|
| 756 |
+
for prompt, category in prompts
|
| 757 |
+
if category in V4_TEACHER_CATEGORIES
|
| 758 |
+
]
|
| 759 |
+
return prompts
|
| 760 |
+
|
| 761 |
+
|
| 762 |
+
def _teacher_system_prompt() -> str:
|
| 763 |
+
if IS_V4:
|
| 764 |
+
return (
|
| 765 |
+
"Você é a assistente de atendimento da Viva Mais. Responda somente com a "
|
| 766 |
+
"resposta final, em português brasileiro natural e com acentos. Não copie "
|
| 767 |
+
"nem transforme a instrução do usuário em uma frase como 'Avise que', "
|
| 768 |
+
"'Pergunte', 'Peça', 'Diga' ou 'Escreva'. Não use preâmbulos como "
|
| 769 |
+
"'Claro' ou 'Aqui está'. Quando houver contexto, use apenas o contexto; "
|
| 770 |
+
"se a informação não aparecer, diga que não consta no contexto e não use "
|
| 771 |
+
"dados de outro cliente."
|
| 772 |
+
)
|
| 773 |
+
return (
|
| 774 |
+
"Você é a assistente de atendimento da Viva Mais. Responda de forma "
|
| 775 |
+
"direta, natural e objetiva em português brasileiro. Não use preâmbulos "
|
| 776 |
+
"como 'Claro, aqui está'. Quando a pergunta trouxer contexto, use apenas "
|
| 777 |
+
"esse contexto e diga quando a informação não estiver presente. Entregue "
|
| 778 |
+
"somente a resposta final, sem explicar estratégia, critérios ou rótulos "
|
| 779 |
+
"como 'Resposta:'."
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
+
|
| 783 |
+
def _usable_teacher_answer(text: str) -> bool:
|
| 784 |
+
cleaned = text.strip()
|
| 785 |
+
lowered = cleaned.lower()
|
| 786 |
+
if not cleaned or len(cleaned) > 800:
|
| 787 |
+
return False
|
| 788 |
+
if IS_V4 and lowered.startswith(INSTRUCTION_ECHO_PREFIXES):
|
| 789 |
+
return False
|
| 790 |
+
if IS_V4 and any(marker in lowered for marker in ACCENTLESS_MARKERS):
|
| 791 |
+
return False
|
| 792 |
+
if lowered.startswith(("claro:", "claro.", "claro,", "claro!", "vai ", "pode ser que")):
|
| 793 |
+
return False
|
| 794 |
+
blocked = (
|
| 795 |
+
"<think",
|
| 796 |
+
"</think",
|
| 797 |
+
"```",
|
| 798 |
+
"###",
|
| 799 |
+
"---",
|
| 800 |
+
"**",
|
| 801 |
+
"😊",
|
| 802 |
+
"✈",
|
| 803 |
+
"claro! aqui",
|
| 804 |
+
"aqui está",
|
| 805 |
+
"aqui vai",
|
| 806 |
+
"posso adaptar",
|
| 807 |
+
"se quiser",
|
| 808 |
+
"como se fosse",
|
| 809 |
+
"vamos ",
|
| 810 |
+
"por exemplo:",
|
| 811 |
+
"a pergunta pede",
|
| 812 |
+
"a frase original",
|
| 813 |
+
"a melhor resposta",
|
| 814 |
+
"a resposta deve",
|
| 815 |
+
"contexto fornecido",
|
| 816 |
+
"não há evidência",
|
| 817 |
+
"o contexto indica",
|
| 818 |
+
"por favor, forneça",
|
| 819 |
+
"resposta:",
|
| 820 |
+
"[seu nome]",
|
| 821 |
+
"[nome da agência]",
|
| 822 |
+
)
|
| 823 |
+
return not any(marker in lowered for marker in blocked)
|
| 824 |
+
|
| 825 |
+
|
| 826 |
+
def _normalize_assistant_accents_in_jsonl(path: Path) -> int:
|
| 827 |
+
if not IS_V4:
|
| 828 |
+
return 0
|
| 829 |
+
rows = _load_jsonl(path)
|
| 830 |
+
changed = 0
|
| 831 |
+
with path.open("w", encoding="utf-8") as handle:
|
| 832 |
+
for row in rows:
|
| 833 |
+
messages = row.get("messages")
|
| 834 |
+
if isinstance(messages, list):
|
| 835 |
+
for message in messages:
|
| 836 |
+
if (
|
| 837 |
+
not isinstance(message, dict)
|
| 838 |
+
or message.get("role") != "assistant"
|
| 839 |
+
):
|
| 840 |
+
continue
|
| 841 |
+
content = str(message.get("content", ""))
|
| 842 |
+
normalized = _normalize_ptbr_accents(content)
|
| 843 |
+
if normalized != content:
|
| 844 |
+
message["content"] = normalized
|
| 845 |
+
changed += 1
|
| 846 |
+
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 847 |
+
return changed
|
| 848 |
+
|
| 849 |
+
|
| 850 |
+
@app.function(
|
| 851 |
+
gpu=GENERATION_GPU,
|
| 852 |
+
image=image,
|
| 853 |
+
volumes={"/data": data_volume},
|
| 854 |
+
timeout=8 * 3600,
|
| 855 |
+
)
|
| 856 |
+
def distill_teacher_rows(
|
| 857 |
+
*,
|
| 858 |
+
teacher_model: str = RIO_TEACHER_MODEL,
|
| 859 |
+
output_path: str = TEACHER_REMOTE,
|
| 860 |
+
target_rows: int = DEFAULT_TEACHER_ROWS,
|
| 861 |
+
gigaverbo_seed_limit: int = DEFAULT_TEACHER_SEED_LIMIT,
|
| 862 |
+
seed: int = 42,
|
| 863 |
+
max_new_tokens: int = 220,
|
| 864 |
+
) -> dict[str, Any]:
|
| 865 |
+
import random
|
| 866 |
+
|
| 867 |
+
import torch
|
| 868 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
|
| 869 |
+
|
| 870 |
+
_ensure_paths()
|
| 871 |
+
tokenizer = AutoTokenizer.from_pretrained(teacher_model, trust_remote_code=True)
|
| 872 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 873 |
+
teacher_model,
|
| 874 |
+
trust_remote_code=True,
|
| 875 |
+
quantization_config=BitsAndBytesConfig(
|
| 876 |
+
load_in_4bit=True,
|
| 877 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 878 |
+
bnb_4bit_quant_type="nf4",
|
| 879 |
+
bnb_4bit_use_double_quant=True,
|
| 880 |
+
),
|
| 881 |
+
device_map={"": 0},
|
| 882 |
+
)
|
| 883 |
+
_configure_tokenizer_for_generation(tokenizer, model)
|
| 884 |
+
model.eval()
|
| 885 |
+
prompts = _teacher_prompt_pool(gigaverbo_limit=gigaverbo_seed_limit, seed=seed)
|
| 886 |
+
random.Random(seed).shuffle(prompts)
|
| 887 |
+
output = Path(output_path)
|
| 888 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 889 |
+
output.write_text("", encoding="utf-8")
|
| 890 |
+
rows: list[dict[str, Any]] = []
|
| 891 |
+
rejected = 0
|
| 892 |
+
for user_prompt, category in prompts:
|
| 893 |
+
if len(rows) >= target_rows:
|
| 894 |
+
break
|
| 895 |
+
messages = [
|
| 896 |
+
{
|
| 897 |
+
"role": "system",
|
| 898 |
+
"content": _teacher_system_prompt(),
|
| 899 |
+
},
|
| 900 |
+
{"role": "user", "content": user_prompt},
|
| 901 |
+
]
|
| 902 |
+
text = tokenizer.apply_chat_template(
|
| 903 |
+
messages,
|
| 904 |
+
tokenize=False,
|
| 905 |
+
add_generation_prompt=True,
|
| 906 |
+
enable_thinking=False,
|
| 907 |
+
)
|
| 908 |
+
inputs = tokenizer(text, return_tensors="pt").to(model.device)
|
| 909 |
+
with torch.no_grad():
|
| 910 |
+
output_ids = model.generate(
|
| 911 |
+
**inputs,
|
| 912 |
+
max_new_tokens=max_new_tokens,
|
| 913 |
+
do_sample=False,
|
| 914 |
+
pad_token_id=tokenizer.pad_token_id,
|
| 915 |
+
)
|
| 916 |
+
answer = tokenizer.decode(
|
| 917 |
+
output_ids[0][inputs["input_ids"].shape[1] :],
|
| 918 |
+
skip_special_tokens=True,
|
| 919 |
+
).strip()
|
| 920 |
+
if IS_V4:
|
| 921 |
+
answer = _normalize_ptbr_accents(answer)
|
| 922 |
+
accepted = _usable_teacher_answer(answer)
|
| 923 |
+
print(
|
| 924 |
+
json.dumps(
|
| 925 |
+
{
|
| 926 |
+
"event": "teacher_output",
|
| 927 |
+
"category": category,
|
| 928 |
+
"accepted": accepted,
|
| 929 |
+
"accepted_count": len(rows),
|
| 930 |
+
"rejected_count": rejected,
|
| 931 |
+
"prompt_preview": _preview(user_prompt, 240),
|
| 932 |
+
"answer_preview": _preview(answer, 500),
|
| 933 |
+
},
|
| 934 |
+
ensure_ascii=False,
|
| 935 |
+
),
|
| 936 |
+
flush=True,
|
| 937 |
+
)
|
| 938 |
+
if not accepted:
|
| 939 |
+
rejected += 1
|
| 940 |
+
continue
|
| 941 |
+
row = {
|
| 942 |
+
"messages": [
|
| 943 |
+
{"role": "user", "content": user_prompt},
|
| 944 |
+
{"role": "assistant", "content": answer},
|
| 945 |
+
],
|
| 946 |
+
"source": "rio31_teacher_distill",
|
| 947 |
+
"seed_category": category,
|
| 948 |
+
"teacher_model": teacher_model,
|
| 949 |
+
}
|
| 950 |
+
rows.append(row)
|
| 951 |
+
_write_jsonl(output, row)
|
| 952 |
+
data_volume.commit()
|
| 953 |
+
data_volume.commit()
|
| 954 |
+
stats = {
|
| 955 |
+
"teacher_model": teacher_model,
|
| 956 |
+
"rows": len(rows),
|
| 957 |
+
"rejected": rejected,
|
| 958 |
+
"output": output_path,
|
| 959 |
+
"target_rows": target_rows,
|
| 960 |
+
}
|
| 961 |
+
print(json.dumps(stats, indent=2, ensure_ascii=False))
|
| 962 |
+
return stats
|
| 963 |
+
|
| 964 |
+
|
| 965 |
+
@app.function(image=image, volumes={"/data": data_volume}, timeout=4 * 3600, cpu=4, memory=16384)
|
| 966 |
+
def prepare_v3_dataset(
|
| 967 |
+
*,
|
| 968 |
+
total_limit: int = DEFAULT_V3_ROWS,
|
| 969 |
+
seed: int = 42,
|
| 970 |
+
teacher_rows_path: str = TEACHER_REMOTE,
|
| 971 |
+
public_weight: float = DEFAULT_PUBLIC_WEIGHT,
|
| 972 |
+
distill_weight: float = DEFAULT_DISTILL_WEIGHT,
|
| 973 |
+
domain_weight: float = DEFAULT_DOMAIN_WEIGHT,
|
| 974 |
+
travel_weight: float = 0.0,
|
| 975 |
+
negative_weight: float = DEFAULT_NEGATIVE_WEIGHT,
|
| 976 |
+
generic_weight: float = DEFAULT_GENERIC_WEIGHT,
|
| 977 |
+
) -> dict[str, Any]:
|
| 978 |
+
_ensure_paths()
|
| 979 |
+
from prepare_minicpm5_v2_distill_sft import (
|
| 980 |
+
DISTILL_BUCKET,
|
| 981 |
+
GENERIC_BUCKET,
|
| 982 |
+
prepare_minicpm5_v2_distill_sft,
|
| 983 |
+
)
|
| 984 |
+
from prepare_minicpm5_text_sft import (
|
| 985 |
+
DOMAIN_BUCKET,
|
| 986 |
+
NEGATIVE_BUCKET,
|
| 987 |
+
PUBLIC_BUCKET,
|
| 988 |
+
TRAVEL_BUCKET,
|
| 989 |
+
)
|
| 990 |
+
|
| 991 |
+
stats = prepare_minicpm5_v2_distill_sft(
|
| 992 |
+
output_path=Path(TRAIN_REMOTE),
|
| 993 |
+
eval_output_path=Path(EVAL_REMOTE),
|
| 994 |
+
teacher_rows_path=Path(teacher_rows_path),
|
| 995 |
+
total_limit=total_limit,
|
| 996 |
+
seed=seed,
|
| 997 |
+
weights={
|
| 998 |
+
PUBLIC_BUCKET: public_weight,
|
| 999 |
+
DISTILL_BUCKET: distill_weight,
|
| 1000 |
+
DOMAIN_BUCKET: domain_weight,
|
| 1001 |
+
TRAVEL_BUCKET: travel_weight,
|
| 1002 |
+
NEGATIVE_BUCKET: negative_weight,
|
| 1003 |
+
GENERIC_BUCKET: generic_weight,
|
| 1004 |
+
},
|
| 1005 |
+
)
|
| 1006 |
+
stats["assistant_accent_normalized_rows"] = _normalize_assistant_accents_in_jsonl(
|
| 1007 |
+
Path(TRAIN_REMOTE)
|
| 1008 |
+
)
|
| 1009 |
+
data_volume.commit()
|
| 1010 |
+
print(json.dumps(stats, indent=2, ensure_ascii=False))
|
| 1011 |
+
return stats
|
| 1012 |
+
|
| 1013 |
+
|
| 1014 |
+
def _load_peft_model(model_name: str, *, context_length: int, load_in_4bit: bool):
|
| 1015 |
+
from unsloth import FastLanguageModel
|
| 1016 |
+
|
| 1017 |
+
return FastLanguageModel.from_pretrained(
|
| 1018 |
+
model_name=model_name,
|
| 1019 |
+
max_seq_length=context_length,
|
| 1020 |
+
dtype=None,
|
| 1021 |
+
load_in_4bit=load_in_4bit,
|
| 1022 |
+
)
|
| 1023 |
+
|
| 1024 |
+
|
| 1025 |
+
def _attach_lora(model, *, rank: int, alpha: int, dropout: float):
|
| 1026 |
+
from unsloth import FastLanguageModel
|
| 1027 |
+
|
| 1028 |
+
return FastLanguageModel.get_peft_model(
|
| 1029 |
+
model,
|
| 1030 |
+
r=rank,
|
| 1031 |
+
target_modules=[
|
| 1032 |
+
"q_proj",
|
| 1033 |
+
"k_proj",
|
| 1034 |
+
"v_proj",
|
| 1035 |
+
"o_proj",
|
| 1036 |
+
"gate_proj",
|
| 1037 |
+
"up_proj",
|
| 1038 |
+
"down_proj",
|
| 1039 |
+
],
|
| 1040 |
+
lora_alpha=alpha,
|
| 1041 |
+
lora_dropout=dropout,
|
| 1042 |
+
use_gradient_checkpointing="unsloth",
|
| 1043 |
+
)
|
| 1044 |
+
|
| 1045 |
+
|
| 1046 |
+
def _render_sft_text(tokenizer, messages: list[dict[str, str]]) -> str:
|
| 1047 |
+
prompt = tokenizer.apply_chat_template(
|
| 1048 |
+
messages[:-1],
|
| 1049 |
+
tokenize=False,
|
| 1050 |
+
add_generation_prompt=True,
|
| 1051 |
+
enable_thinking=False,
|
| 1052 |
+
)
|
| 1053 |
+
answer = str(messages[-1].get("content", "")).strip()
|
| 1054 |
+
return f"{prompt}{answer}<|im_end|>\n"
|
| 1055 |
+
|
| 1056 |
+
|
| 1057 |
+
@app.function(
|
| 1058 |
+
gpu=TRAIN_GPU,
|
| 1059 |
+
image=image,
|
| 1060 |
+
volumes={"/data": data_volume},
|
| 1061 |
+
timeout=12 * 3600,
|
| 1062 |
+
)
|
| 1063 |
+
def train_sft(
|
| 1064 |
+
*,
|
| 1065 |
+
base_model: str = V1_MODEL,
|
| 1066 |
+
output_dir: str = LORA_DIR,
|
| 1067 |
+
context_length: int = DEFAULT_CONTEXT_LENGTH,
|
| 1068 |
+
num_epochs: float = DEFAULT_EPOCHS,
|
| 1069 |
+
learning_rate: float = DEFAULT_LEARNING_RATE,
|
| 1070 |
+
per_device_batch_size: int = 2,
|
| 1071 |
+
gradient_accumulation_steps: int = 8,
|
| 1072 |
+
lora_rank: int = 16,
|
| 1073 |
+
lora_alpha: int = 32,
|
| 1074 |
+
) -> str:
|
| 1075 |
+
os.environ["UNSLOTH_COMPILE_DISABLE"] = "1"
|
| 1076 |
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 1077 |
+
from datasets import load_dataset
|
| 1078 |
+
from peft import PeftModel
|
| 1079 |
+
from transformers import DataCollatorForLanguageModeling, Trainer, TrainingArguments
|
| 1080 |
+
from unsloth.chat_templates import train_on_responses_only
|
| 1081 |
+
|
| 1082 |
+
model, tokenizer = _load_peft_model(
|
| 1083 |
+
base_model,
|
| 1084 |
+
context_length=context_length,
|
| 1085 |
+
load_in_4bit=True,
|
| 1086 |
+
)
|
| 1087 |
+
_configure_tokenizer_for_generation(tokenizer, model)
|
| 1088 |
+
if isinstance(model, PeftModel):
|
| 1089 |
+
raise RuntimeError(f"Base model {base_model!r} still has LoRA adapters attached")
|
| 1090 |
+
model = _attach_lora(model, rank=lora_rank, alpha=lora_alpha, dropout=0.05)
|
| 1091 |
+
|
| 1092 |
+
dataset = load_dataset("json", data_files=TRAIN_REMOTE, split="train")
|
| 1093 |
+
print(f"Loaded {len(dataset)} examples from {TRAIN_REMOTE}")
|
| 1094 |
+
|
| 1095 |
+
def to_text(example: dict) -> dict:
|
| 1096 |
+
return {"text": _render_sft_text(tokenizer, example["messages"])}
|
| 1097 |
+
|
| 1098 |
+
dataset = dataset.map(to_text, remove_columns=dataset.column_names, num_proc=1)
|
| 1099 |
+
|
| 1100 |
+
def tokenize(example: dict) -> dict:
|
| 1101 |
+
return tokenizer(example["text"], truncation=True, max_length=context_length)
|
| 1102 |
+
|
| 1103 |
+
dataset = dataset.map(tokenize, remove_columns=["text"], num_proc=1)
|
| 1104 |
+
collator = DataCollatorForLanguageModeling(
|
| 1105 |
+
tokenizer=tokenizer,
|
| 1106 |
+
mlm=False,
|
| 1107 |
+
pad_to_multiple_of=8,
|
| 1108 |
+
)
|
| 1109 |
+
trainer = Trainer(
|
| 1110 |
+
model=model,
|
| 1111 |
+
processing_class=tokenizer,
|
| 1112 |
+
train_dataset=dataset,
|
| 1113 |
+
data_collator=collator,
|
| 1114 |
+
args=TrainingArguments(
|
| 1115 |
+
output_dir=output_dir,
|
| 1116 |
+
per_device_train_batch_size=per_device_batch_size,
|
| 1117 |
+
gradient_accumulation_steps=gradient_accumulation_steps,
|
| 1118 |
+
num_train_epochs=num_epochs,
|
| 1119 |
+
learning_rate=learning_rate,
|
| 1120 |
+
warmup_ratio=0.03,
|
| 1121 |
+
lr_scheduler_type="cosine",
|
| 1122 |
+
bf16=True,
|
| 1123 |
+
logging_steps=20,
|
| 1124 |
+
save_strategy="steps",
|
| 1125 |
+
save_steps=500,
|
| 1126 |
+
save_total_limit=2,
|
| 1127 |
+
dataloader_num_workers=0,
|
| 1128 |
+
dataloader_pin_memory=False,
|
| 1129 |
+
report_to="none",
|
| 1130 |
+
remove_unused_columns=False,
|
| 1131 |
+
),
|
| 1132 |
+
)
|
| 1133 |
+
trainer = train_on_responses_only(
|
| 1134 |
+
trainer,
|
| 1135 |
+
instruction_part="<|im_start|>user\n",
|
| 1136 |
+
response_part=NO_THINKING_RESPONSE_PREFIX,
|
| 1137 |
+
num_proc=1,
|
| 1138 |
+
)
|
| 1139 |
+
trainer.train()
|
| 1140 |
+
model.save_pretrained(output_dir)
|
| 1141 |
+
tokenizer.save_pretrained(output_dir)
|
| 1142 |
+
data_volume.commit()
|
| 1143 |
+
return output_dir
|
| 1144 |
+
|
| 1145 |
+
|
| 1146 |
+
@app.function(
|
| 1147 |
+
gpu=TRAIN_GPU,
|
| 1148 |
+
image=image,
|
| 1149 |
+
volumes={"/data": data_volume},
|
| 1150 |
+
timeout=4 * 3600,
|
| 1151 |
+
)
|
| 1152 |
+
def merge_lora(
|
| 1153 |
+
*,
|
| 1154 |
+
lora_dir: str = LORA_DIR,
|
| 1155 |
+
merged_dir: str = MERGED_DIR,
|
| 1156 |
+
context_length: int = DEFAULT_CONTEXT_LENGTH,
|
| 1157 |
+
) -> str:
|
| 1158 |
+
from unsloth import FastLanguageModel
|
| 1159 |
+
|
| 1160 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 1161 |
+
model_name=lora_dir,
|
| 1162 |
+
max_seq_length=context_length,
|
| 1163 |
+
dtype=None,
|
| 1164 |
+
load_in_4bit=False,
|
| 1165 |
+
)
|
| 1166 |
+
model.save_pretrained_merged(merged_dir, tokenizer, save_method="merged_16bit")
|
| 1167 |
+
data_volume.commit()
|
| 1168 |
+
return merged_dir
|
| 1169 |
+
|
| 1170 |
+
|
| 1171 |
+
def _generate_answer(
|
| 1172 |
+
*,
|
| 1173 |
+
model,
|
| 1174 |
+
tokenizer,
|
| 1175 |
+
messages: list[dict[str, Any]],
|
| 1176 |
+
max_new_tokens: int,
|
| 1177 |
+
do_sample: bool = False,
|
| 1178 |
+
) -> str:
|
| 1179 |
+
import torch
|
| 1180 |
+
|
| 1181 |
+
_configure_tokenizer_for_generation(tokenizer, model)
|
| 1182 |
+
text = tokenizer.apply_chat_template(
|
| 1183 |
+
messages,
|
| 1184 |
+
tokenize=False,
|
| 1185 |
+
add_generation_prompt=True,
|
| 1186 |
+
enable_thinking=False,
|
| 1187 |
+
)
|
| 1188 |
+
inputs = tokenizer(text, return_tensors="pt").to(model.device)
|
| 1189 |
+
generation_kwargs: dict[str, Any] = {
|
| 1190 |
+
"max_new_tokens": max_new_tokens,
|
| 1191 |
+
"do_sample": do_sample,
|
| 1192 |
+
"pad_token_id": tokenizer.pad_token_id,
|
| 1193 |
+
"eos_token_id": tokenizer.eos_token_id,
|
| 1194 |
+
}
|
| 1195 |
+
if do_sample:
|
| 1196 |
+
generation_kwargs["temperature"] = 0.7
|
| 1197 |
+
generation_kwargs["top_p"] = 0.9
|
| 1198 |
+
with torch.no_grad():
|
| 1199 |
+
output_ids = model.generate(**inputs, **generation_kwargs)
|
| 1200 |
+
return tokenizer.decode(
|
| 1201 |
+
output_ids[0][inputs["input_ids"].shape[1] :],
|
| 1202 |
+
skip_special_tokens=True,
|
| 1203 |
+
)
|
| 1204 |
+
|
| 1205 |
+
|
| 1206 |
+
@app.function(
|
| 1207 |
+
gpu=GENERATION_GPU,
|
| 1208 |
+
image=image,
|
| 1209 |
+
volumes={"/data": data_volume, "/results": results_volume},
|
| 1210 |
+
timeout=4 * 3600,
|
| 1211 |
+
)
|
| 1212 |
+
def eval_vivamais(
|
| 1213 |
+
*,
|
| 1214 |
+
model_id: str,
|
| 1215 |
+
run_name: str,
|
| 1216 |
+
max_new_tokens: int = 160,
|
| 1217 |
+
) -> dict[str, Any]:
|
| 1218 |
+
import torch
|
| 1219 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 1220 |
+
|
| 1221 |
+
_ensure_paths()
|
| 1222 |
+
import sys
|
| 1223 |
+
|
| 1224 |
+
sys.path.insert(0, "/opt/vivamais/evals")
|
| 1225 |
+
from vivamais_qa_scoring import score_case, summarize_results
|
| 1226 |
+
|
| 1227 |
+
eval_file = Path(EVAL_REMOTE)
|
| 1228 |
+
cases = _load_jsonl(eval_file)
|
| 1229 |
+
if len(cases) < 120:
|
| 1230 |
+
from build_vivamais_qa_eval import build_vivamais_qa_eval
|
| 1231 |
+
|
| 1232 |
+
build_vivamais_qa_eval(eval_file)
|
| 1233 |
+
data_volume.commit()
|
| 1234 |
+
cases = _load_jsonl(eval_file)
|
| 1235 |
+
output_dir = Path("/results") / run_name
|
| 1236 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 1237 |
+
output_log_path = output_dir / "outputs.jsonl"
|
| 1238 |
+
output_log_path.write_text("", encoding="utf-8")
|
| 1239 |
+
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
| 1240 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 1241 |
+
model_id,
|
| 1242 |
+
trust_remote_code=True,
|
| 1243 |
+
torch_dtype=torch.bfloat16,
|
| 1244 |
+
device_map="auto",
|
| 1245 |
+
)
|
| 1246 |
+
model.eval()
|
| 1247 |
+
predictions = []
|
| 1248 |
+
for case_number, case in enumerate(cases, start=1):
|
| 1249 |
+
prediction = _generate_answer(
|
| 1250 |
+
model=model,
|
| 1251 |
+
tokenizer=tokenizer,
|
| 1252 |
+
messages=case["messages"],
|
| 1253 |
+
max_new_tokens=max_new_tokens,
|
| 1254 |
+
)
|
| 1255 |
+
scored = score_case(case, prediction)
|
| 1256 |
+
row = {
|
| 1257 |
+
"id": case.get("id"),
|
| 1258 |
+
"category": case.get("category"),
|
| 1259 |
+
"case": case,
|
| 1260 |
+
"prediction": scored["prediction"],
|
| 1261 |
+
"score": scored["score"],
|
| 1262 |
+
"passed": scored["passed"],
|
| 1263 |
+
"forbidden_hits": scored["forbidden_hits"],
|
| 1264 |
+
"unknown_gate": scored["unknown_gate"],
|
| 1265 |
+
"expected_answer": case.get("expected_answer"),
|
| 1266 |
+
}
|
| 1267 |
+
predictions.append(row)
|
| 1268 |
+
_write_jsonl(output_log_path, row)
|
| 1269 |
+
print(
|
| 1270 |
+
json.dumps(
|
| 1271 |
+
{
|
| 1272 |
+
"event": "vivamais_eval_output",
|
| 1273 |
+
"run_name": run_name,
|
| 1274 |
+
"case_number": case_number,
|
| 1275 |
+
"total_cases": len(cases),
|
| 1276 |
+
"id": row["id"],
|
| 1277 |
+
"category": row["category"],
|
| 1278 |
+
"passed": row["passed"],
|
| 1279 |
+
"score": row["score"],
|
| 1280 |
+
"forbidden_hits": row["forbidden_hits"],
|
| 1281 |
+
"unknown_gate": row["unknown_gate"],
|
| 1282 |
+
"prediction_preview": _preview(row["prediction"]),
|
| 1283 |
+
},
|
| 1284 |
+
ensure_ascii=False,
|
| 1285 |
+
),
|
| 1286 |
+
flush=True,
|
| 1287 |
+
)
|
| 1288 |
+
payload = {
|
| 1289 |
+
"model_name": model_id,
|
| 1290 |
+
"run_name": run_name,
|
| 1291 |
+
"generation": {"enable_thinking": False, "max_new_tokens": max_new_tokens},
|
| 1292 |
+
"predictions": predictions,
|
| 1293 |
+
}
|
| 1294 |
+
summary = summarize_results(payload)
|
| 1295 |
+
(output_dir / "results.json").write_text(json.dumps(payload, indent=2, ensure_ascii=False))
|
| 1296 |
+
(output_dir / "summary.json").write_text(json.dumps(summary, indent=2, ensure_ascii=False))
|
| 1297 |
+
results_volume.commit()
|
| 1298 |
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
| 1299 |
+
return summary
|
| 1300 |
+
|
| 1301 |
+
|
| 1302 |
+
@app.function(
|
| 1303 |
+
gpu=GENERATION_GPU,
|
| 1304 |
+
image=image,
|
| 1305 |
+
volumes={"/data": data_volume, "/results": results_volume},
|
| 1306 |
+
timeout=2 * 3600,
|
| 1307 |
+
)
|
| 1308 |
+
def eval_generic_ptbr(
|
| 1309 |
+
*,
|
| 1310 |
+
model_id: str,
|
| 1311 |
+
run_name: str,
|
| 1312 |
+
max_new_tokens: int = 180,
|
| 1313 |
+
) -> dict[str, Any]:
|
| 1314 |
+
import torch
|
| 1315 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 1316 |
+
|
| 1317 |
+
_ensure_paths()
|
| 1318 |
+
from ptbr_conversation_eval import (
|
| 1319 |
+
GENERIC_EVAL_CASES,
|
| 1320 |
+
score_generic_case,
|
| 1321 |
+
summarize_generic_scores,
|
| 1322 |
+
)
|
| 1323 |
+
|
| 1324 |
+
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
| 1325 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 1326 |
+
model_id,
|
| 1327 |
+
trust_remote_code=True,
|
| 1328 |
+
torch_dtype=torch.bfloat16,
|
| 1329 |
+
device_map="auto",
|
| 1330 |
+
)
|
| 1331 |
+
model.eval()
|
| 1332 |
+
scored = []
|
| 1333 |
+
output_dir = Path("/results") / run_name
|
| 1334 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 1335 |
+
output_log_path = output_dir / "outputs.jsonl"
|
| 1336 |
+
output_log_path.write_text("", encoding="utf-8")
|
| 1337 |
+
for case_number, case in enumerate(GENERIC_EVAL_CASES, start=1):
|
| 1338 |
+
prediction = _generate_answer(
|
| 1339 |
+
model=model,
|
| 1340 |
+
tokenizer=tokenizer,
|
| 1341 |
+
messages=case["messages"],
|
| 1342 |
+
max_new_tokens=max_new_tokens,
|
| 1343 |
+
)
|
| 1344 |
+
row = score_generic_case(case, prediction)
|
| 1345 |
+
row["case"] = case
|
| 1346 |
+
scored.append(row)
|
| 1347 |
+
_write_jsonl(output_log_path, row)
|
| 1348 |
+
print(
|
| 1349 |
+
json.dumps(
|
| 1350 |
+
{
|
| 1351 |
+
"event": "generic_eval_output",
|
| 1352 |
+
"run_name": run_name,
|
| 1353 |
+
"case_number": case_number,
|
| 1354 |
+
"total_cases": len(GENERIC_EVAL_CASES),
|
| 1355 |
+
"id": row["id"],
|
| 1356 |
+
"category": row["category"],
|
| 1357 |
+
"passed": row["passed"],
|
| 1358 |
+
"score": row["score"],
|
| 1359 |
+
"forbidden_hits": row["forbidden_hits"],
|
| 1360 |
+
"prediction_preview": _preview(row["prediction"]),
|
| 1361 |
+
},
|
| 1362 |
+
ensure_ascii=False,
|
| 1363 |
+
),
|
| 1364 |
+
flush=True,
|
| 1365 |
+
)
|
| 1366 |
+
summary = summarize_generic_scores(model=model_id, run_name=run_name, scored=scored)
|
| 1367 |
+
(output_dir / "results.json").write_text(
|
| 1368 |
+
json.dumps({"summary": summary, "predictions": scored}, indent=2, ensure_ascii=False)
|
| 1369 |
+
)
|
| 1370 |
+
(output_dir / "summary.json").write_text(json.dumps(summary, indent=2, ensure_ascii=False))
|
| 1371 |
+
results_volume.commit()
|
| 1372 |
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
| 1373 |
+
return summary
|
| 1374 |
+
|
| 1375 |
+
|
| 1376 |
+
def _candidate_passes(
|
| 1377 |
+
*,
|
| 1378 |
+
baseline_vivamais: dict[str, Any],
|
| 1379 |
+
candidate_vivamais: dict[str, Any],
|
| 1380 |
+
baseline_generic: dict[str, Any],
|
| 1381 |
+
candidate_generic: dict[str, Any],
|
| 1382 |
+
min_dashboard_delta: float = V3_MIN_DASHBOARD_DELTA,
|
| 1383 |
+
min_generic_score: float = V3_MIN_GENERIC_SCORE,
|
| 1384 |
+
) -> bool:
|
| 1385 |
+
dashboard_passed = (
|
| 1386 |
+
candidate_vivamais["average_score"]
|
| 1387 |
+
>= baseline_vivamais["average_score"] + min_dashboard_delta
|
| 1388 |
+
and candidate_vivamais["unknown_failures"] <= baseline_vivamais["unknown_failures"]
|
| 1389 |
+
and candidate_vivamais["leakage_failures"] <= baseline_vivamais["leakage_failures"]
|
| 1390 |
+
)
|
| 1391 |
+
if IS_V4:
|
| 1392 |
+
return dashboard_passed and candidate_generic["average_score"] >= min_generic_score
|
| 1393 |
+
return (
|
| 1394 |
+
dashboard_passed
|
| 1395 |
+
and candidate_generic["average_score"] > baseline_generic["average_score"]
|
| 1396 |
+
and candidate_generic["average_score"] >= min_generic_score
|
| 1397 |
+
)
|
| 1398 |
+
|
| 1399 |
+
|
| 1400 |
+
@app.function(image=image, volumes={"/results": results_volume}, timeout=10 * 60)
|
| 1401 |
+
def show_eval_details(
|
| 1402 |
+
*,
|
| 1403 |
+
run_names: list[str],
|
| 1404 |
+
failed_only: bool = True,
|
| 1405 |
+
max_prediction_chars: int = 600,
|
| 1406 |
+
) -> dict[str, Any]:
|
| 1407 |
+
summaries: dict[str, Any] = {}
|
| 1408 |
+
for run_name in run_names:
|
| 1409 |
+
results_path = Path("/results") / run_name / "results.json"
|
| 1410 |
+
if not results_path.is_file():
|
| 1411 |
+
summaries[run_name] = {"error": f"Missing results file: {results_path}"}
|
| 1412 |
+
continue
|
| 1413 |
+
payload = json.loads(results_path.read_text(encoding="utf-8"))
|
| 1414 |
+
rows = payload.get("predictions", [])
|
| 1415 |
+
details = []
|
| 1416 |
+
for row in rows:
|
| 1417 |
+
passed = bool(row.get("passed"))
|
| 1418 |
+
if failed_only and passed:
|
| 1419 |
+
continue
|
| 1420 |
+
case = row.get("case", {})
|
| 1421 |
+
if not isinstance(case, dict):
|
| 1422 |
+
case = {}
|
| 1423 |
+
prediction = str(row.get("prediction", "")).strip()
|
| 1424 |
+
details.append(
|
| 1425 |
+
{
|
| 1426 |
+
"id": row.get("id") or case.get("id"),
|
| 1427 |
+
"category": row.get("category") or case.get("category"),
|
| 1428 |
+
"passed": passed,
|
| 1429 |
+
"score": row.get("score"),
|
| 1430 |
+
"forbidden_hits": row.get("forbidden_hits", []),
|
| 1431 |
+
"unknown_gate": row.get("unknown_gate"),
|
| 1432 |
+
"expected_answer": row.get("expected_answer") or case.get("expected_answer"),
|
| 1433 |
+
"prediction": prediction[:max_prediction_chars],
|
| 1434 |
+
}
|
| 1435 |
+
)
|
| 1436 |
+
summaries[run_name] = {
|
| 1437 |
+
"summary": payload.get("summary"),
|
| 1438 |
+
"details": details,
|
| 1439 |
+
}
|
| 1440 |
+
print(json.dumps(summaries, indent=2, ensure_ascii=False))
|
| 1441 |
+
return summaries
|
| 1442 |
+
|
| 1443 |
+
|
| 1444 |
+
@app.function(image=image, volumes={"/data": data_volume}, timeout=4 * 3600, cpu=4, memory=16384)
|
| 1445 |
+
def publish_model(
|
| 1446 |
+
*,
|
| 1447 |
+
repo_id: str = V3_MODEL_REPO,
|
| 1448 |
+
source_dir: str = MERGED_DIR,
|
| 1449 |
+
base_model: str = V1_MODEL,
|
| 1450 |
+
hf_token: str,
|
| 1451 |
+
) -> dict[str, str]:
|
| 1452 |
+
from huggingface_hub import HfApi
|
| 1453 |
+
|
| 1454 |
+
token = hf_token.strip()
|
| 1455 |
+
if not token:
|
| 1456 |
+
raise RuntimeError(f"HF token is required to publish {TEXT_VARIANT}")
|
| 1457 |
+
source = Path(source_dir)
|
| 1458 |
+
readme = source / "README.md"
|
| 1459 |
+
readme.write_text(
|
| 1460 |
+
f"""---
|
| 1461 |
+
language:
|
| 1462 |
+
- pt
|
| 1463 |
+
license: apache-2.0
|
| 1464 |
+
base_model: {base_model}
|
| 1465 |
+
library_name: transformers
|
| 1466 |
+
tags:
|
| 1467 |
+
- brazilian-portuguese
|
| 1468 |
+
- minicpm5
|
| 1469 |
+
- vivamais
|
| 1470 |
+
- distilled
|
| 1471 |
+
- conversational
|
| 1472 |
+
- grounded-qa
|
| 1473 |
+
---
|
| 1474 |
+
|
| 1475 |
+
# {repo_id.split("/")[-1]}
|
| 1476 |
+
|
| 1477 |
+
MiniCPM5-1B Viva Mais text {TEXT_VARIANT}, trained from `{base_model}` with a
|
| 1478 |
+
broader Brazilian Portuguese conversational and grounded-Q&A mix.
|
| 1479 |
+
|
| 1480 |
+
The training package uses a redacted Viva Mais dashboard Q&A replay set,
|
| 1481 |
+
grounding-negative rows, and filtered Rio 3.1 teacher-distilled rows. The v4
|
| 1482 |
+
variant intentionally excludes the generic WhatsApp-style bucket and uses a
|
| 1483 |
+
stricter final-answer-only teacher prompt. Teacher rows are filtered to reject
|
| 1484 |
+
verbose wrappers and hidden-reasoning artifacts. No raw WhatsApp exports, client
|
| 1485 |
+
identifiers, identity documents, or local private data are published.
|
| 1486 |
+
""",
|
| 1487 |
+
encoding="utf-8",
|
| 1488 |
+
)
|
| 1489 |
+
api = HfApi(token=token)
|
| 1490 |
+
api.create_repo(repo_id=repo_id, repo_type="model", exist_ok=True)
|
| 1491 |
+
api.upload_folder(
|
| 1492 |
+
folder_path=str(source),
|
| 1493 |
+
repo_id=repo_id,
|
| 1494 |
+
repo_type="model",
|
| 1495 |
+
commit_message=f"Publish MiniCPM5 Viva Mais text {TEXT_VARIANT}",
|
| 1496 |
+
)
|
| 1497 |
+
return {"repo": repo_id, "url": f"https://huggingface.co/{repo_id}"}
|
| 1498 |
+
|
| 1499 |
+
|
| 1500 |
+
@app.function(gpu="A100", image=image, volumes={"/data": data_volume}, timeout=6 * 3600)
|
| 1501 |
+
def export_and_publish_gguf(
|
| 1502 |
+
*,
|
| 1503 |
+
source_model: str = V3_MODEL_REPO,
|
| 1504 |
+
repo_id: str = V3_GGUF_REPO,
|
| 1505 |
+
quant: str = "q4_k_m",
|
| 1506 |
+
context_length: int = DEFAULT_CONTEXT_LENGTH,
|
| 1507 |
+
hf_token: str,
|
| 1508 |
+
) -> dict[str, Any]:
|
| 1509 |
+
import shutil
|
| 1510 |
+
|
| 1511 |
+
from huggingface_hub import HfApi
|
| 1512 |
+
from unsloth import FastLanguageModel
|
| 1513 |
+
|
| 1514 |
+
token = hf_token.strip()
|
| 1515 |
+
if not token:
|
| 1516 |
+
raise RuntimeError(f"HF token is required to publish {TEXT_VARIANT} GGUF")
|
| 1517 |
+
output_dir = Path(GGUF_DIR)
|
| 1518 |
+
gguf_output_dir = Path(f"{GGUF_DIR}_gguf")
|
| 1519 |
+
if output_dir.exists():
|
| 1520 |
+
shutil.rmtree(output_dir)
|
| 1521 |
+
if gguf_output_dir.exists():
|
| 1522 |
+
shutil.rmtree(gguf_output_dir)
|
| 1523 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 1524 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 1525 |
+
model_name=source_model,
|
| 1526 |
+
max_seq_length=context_length,
|
| 1527 |
+
dtype=None,
|
| 1528 |
+
load_in_4bit=False,
|
| 1529 |
+
)
|
| 1530 |
+
FastLanguageModel.for_inference(model)
|
| 1531 |
+
model.save_pretrained_gguf(str(output_dir), tokenizer, quantization_method=quant)
|
| 1532 |
+
gguf_files = sorted(gguf_output_dir.glob("*.gguf"))
|
| 1533 |
+
if len(gguf_files) != 1:
|
| 1534 |
+
found = ", ".join(path.name for path in gguf_files) or "none"
|
| 1535 |
+
raise RuntimeError(f"Expected one GGUF file in {gguf_output_dir}, found {found}")
|
| 1536 |
+
target_name = f"{source_model.split('/')[-1]}-{quant.upper()}.gguf"
|
| 1537 |
+
target_path = gguf_output_dir / target_name
|
| 1538 |
+
if gguf_files[0] != target_path:
|
| 1539 |
+
gguf_files[0].rename(target_path)
|
| 1540 |
+
(gguf_output_dir / "README.md").write_text(
|
| 1541 |
+
f"""---
|
| 1542 |
+
language:
|
| 1543 |
+
- pt
|
| 1544 |
+
license: apache-2.0
|
| 1545 |
+
base_model: {source_model}
|
| 1546 |
+
library_name: gguf
|
| 1547 |
+
tags:
|
| 1548 |
+
- gguf
|
| 1549 |
+
- llama-cpp
|
| 1550 |
+
- minicpm5
|
| 1551 |
+
- vivamais
|
| 1552 |
+
- brazilian-portuguese
|
| 1553 |
+
---
|
| 1554 |
+
|
| 1555 |
+
# {repo_id.split("/")[-1]}
|
| 1556 |
+
|
| 1557 |
+
GGUF export of `{source_model}` for llama.cpp-compatible Viva Mais inference.
|
| 1558 |
+
|
| 1559 |
+
- Quantization: `{quant.upper()}`
|
| 1560 |
+
- File: `{target_name}`
|
| 1561 |
+
""",
|
| 1562 |
+
encoding="utf-8",
|
| 1563 |
+
)
|
| 1564 |
+
data_volume.commit()
|
| 1565 |
+
api = HfApi(token=token)
|
| 1566 |
+
api.create_repo(repo_id=repo_id, repo_type="model", exist_ok=True)
|
| 1567 |
+
api.upload_folder(
|
| 1568 |
+
folder_path=str(gguf_output_dir),
|
| 1569 |
+
repo_id=repo_id,
|
| 1570 |
+
repo_type="model",
|
| 1571 |
+
commit_message=(
|
| 1572 |
+
f"Publish {quant.upper()} GGUF for MiniCPM5 Viva Mais {TEXT_VARIANT}"
|
| 1573 |
+
),
|
| 1574 |
+
)
|
| 1575 |
+
return {
|
| 1576 |
+
"repo": repo_id,
|
| 1577 |
+
"url": f"https://huggingface.co/{repo_id}",
|
| 1578 |
+
"file": target_name,
|
| 1579 |
+
"size_bytes": target_path.stat().st_size,
|
| 1580 |
+
}
|
| 1581 |
+
|
| 1582 |
+
|
| 1583 |
+
@app.local_entrypoint()
|
| 1584 |
+
def main(
|
| 1585 |
+
*,
|
| 1586 |
+
step: str = "candidate",
|
| 1587 |
+
run_name: str = DEFAULT_RUN_NAME,
|
| 1588 |
+
teacher_model: str = RIO_TEACHER_MODEL,
|
| 1589 |
+
teacher_rows: int = DEFAULT_TEACHER_ROWS,
|
| 1590 |
+
teacher_gigaverbo_seed_limit: int = DEFAULT_TEACHER_SEED_LIMIT,
|
| 1591 |
+
teacher_max_new_tokens: int = 180,
|
| 1592 |
+
total_limit: int = DEFAULT_V3_ROWS,
|
| 1593 |
+
public_weight: float = DEFAULT_PUBLIC_WEIGHT,
|
| 1594 |
+
distill_weight: float = DEFAULT_DISTILL_WEIGHT,
|
| 1595 |
+
domain_weight: float = DEFAULT_DOMAIN_WEIGHT,
|
| 1596 |
+
travel_weight: float = 0.0,
|
| 1597 |
+
negative_weight: float = DEFAULT_NEGATIVE_WEIGHT,
|
| 1598 |
+
generic_weight: float = DEFAULT_GENERIC_WEIGHT,
|
| 1599 |
+
learning_rate: float = DEFAULT_LEARNING_RATE,
|
| 1600 |
+
num_epochs: float = DEFAULT_EPOCHS,
|
| 1601 |
+
baseline_eval_model: str = V1_MODEL,
|
| 1602 |
+
candidate_eval_model: str = "",
|
| 1603 |
+
details_run_names: str = "",
|
| 1604 |
+
publish: bool = False,
|
| 1605 |
+
publish_gguf: bool = False,
|
| 1606 |
+
) -> None:
|
| 1607 |
+
if step in {"publish", "gguf"}:
|
| 1608 |
+
raise RuntimeError(
|
| 1609 |
+
"Direct publish steps are disabled. Run --step eval with --publish and/or "
|
| 1610 |
+
"--publish-gguf so the v1 comparison gates run in the same invocation."
|
| 1611 |
+
)
|
| 1612 |
+
if step in {"candidate", "distill"}:
|
| 1613 |
+
stats = distill_teacher_rows.remote(
|
| 1614 |
+
teacher_model=teacher_model,
|
| 1615 |
+
target_rows=teacher_rows,
|
| 1616 |
+
gigaverbo_seed_limit=teacher_gigaverbo_seed_limit,
|
| 1617 |
+
max_new_tokens=teacher_max_new_tokens,
|
| 1618 |
+
)
|
| 1619 |
+
print(f"Teacher distill stats: {stats}")
|
| 1620 |
+
if step in {"candidate", "prepare"}:
|
| 1621 |
+
stats = prepare_v3_dataset.remote(
|
| 1622 |
+
total_limit=total_limit,
|
| 1623 |
+
public_weight=public_weight,
|
| 1624 |
+
distill_weight=distill_weight,
|
| 1625 |
+
domain_weight=domain_weight,
|
| 1626 |
+
travel_weight=travel_weight,
|
| 1627 |
+
negative_weight=negative_weight,
|
| 1628 |
+
generic_weight=generic_weight,
|
| 1629 |
+
)
|
| 1630 |
+
print(f"Dataset stats: {stats}")
|
| 1631 |
+
if step in {"candidate", "train"}:
|
| 1632 |
+
lora = train_sft.remote(learning_rate=learning_rate, num_epochs=num_epochs)
|
| 1633 |
+
print(f"LoRA saved to {lora}")
|
| 1634 |
+
if step in {"candidate", "merge"}:
|
| 1635 |
+
merged = merge_lora.remote()
|
| 1636 |
+
print(f"Merged model saved to {merged}")
|
| 1637 |
+
if step == "inspect-evals":
|
| 1638 |
+
summary = inspect_eval_suites.remote()
|
| 1639 |
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
| 1640 |
+
if step == "check-evals":
|
| 1641 |
+
result = run_eval_unit_subset.remote()
|
| 1642 |
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
| 1643 |
+
if step in {"candidate", "eval"}:
|
| 1644 |
+
selected_candidate = candidate_eval_model or MERGED_DIR
|
| 1645 |
+
baseline_vivamais = eval_vivamais.remote(
|
| 1646 |
+
model_id=baseline_eval_model,
|
| 1647 |
+
run_name=f"{run_name}_baseline_vivamais",
|
| 1648 |
+
)
|
| 1649 |
+
candidate_vivamais = eval_vivamais.remote(
|
| 1650 |
+
model_id=selected_candidate,
|
| 1651 |
+
run_name=f"{run_name}_candidate_vivamais",
|
| 1652 |
+
)
|
| 1653 |
+
baseline_generic = eval_generic_ptbr.remote(
|
| 1654 |
+
model_id=baseline_eval_model,
|
| 1655 |
+
run_name=f"{run_name}_baseline_generic",
|
| 1656 |
+
)
|
| 1657 |
+
candidate_generic = eval_generic_ptbr.remote(
|
| 1658 |
+
model_id=selected_candidate,
|
| 1659 |
+
run_name=f"{run_name}_candidate_generic",
|
| 1660 |
+
)
|
| 1661 |
+
decision = _candidate_passes(
|
| 1662 |
+
baseline_vivamais=baseline_vivamais,
|
| 1663 |
+
candidate_vivamais=candidate_vivamais,
|
| 1664 |
+
baseline_generic=baseline_generic,
|
| 1665 |
+
candidate_generic=candidate_generic,
|
| 1666 |
+
)
|
| 1667 |
+
print(
|
| 1668 |
+
json.dumps(
|
| 1669 |
+
{
|
| 1670 |
+
"accepted": decision,
|
| 1671 |
+
"baseline_vivamais": baseline_vivamais,
|
| 1672 |
+
"candidate_vivamais": candidate_vivamais,
|
| 1673 |
+
"baseline_generic": baseline_generic,
|
| 1674 |
+
"candidate_generic": candidate_generic,
|
| 1675 |
+
},
|
| 1676 |
+
indent=2,
|
| 1677 |
+
ensure_ascii=False,
|
| 1678 |
+
)
|
| 1679 |
+
)
|
| 1680 |
+
if not decision and (publish or publish_gguf):
|
| 1681 |
+
raise RuntimeError("Candidate did not pass v3 publish gates")
|
| 1682 |
+
if step == "details":
|
| 1683 |
+
run_names = [part.strip() for part in details_run_names.split(",") if part.strip()]
|
| 1684 |
+
if not run_names:
|
| 1685 |
+
raise RuntimeError("--details-run-names is required for --step details")
|
| 1686 |
+
show_eval_details.remote(run_names=run_names)
|
| 1687 |
+
if step == "publish" or publish:
|
| 1688 |
+
published = publish_model.remote(
|
| 1689 |
+
repo_id=V3_MODEL_REPO,
|
| 1690 |
+
source_dir=MERGED_DIR,
|
| 1691 |
+
hf_token=os.environ.get("HF_TOKEN", ""),
|
| 1692 |
+
)
|
| 1693 |
+
print(f"Published model: {published}")
|
| 1694 |
+
if step == "gguf" or publish_gguf:
|
| 1695 |
+
published_gguf = export_and_publish_gguf.remote(
|
| 1696 |
+
source_model=V3_MODEL_REPO,
|
| 1697 |
+
repo_id=V3_GGUF_REPO,
|
| 1698 |
+
hf_token=os.environ.get("HF_TOKEN", ""),
|
| 1699 |
+
)
|
| 1700 |
+
print(f"Published GGUF: {published_gguf}")
|
modal/restore_vivamais_v3_dataset_volume.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Restore Viva Mais MiniCPM5 text v3 data volume from the HF dataset package."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import shutil
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import modal
|
| 12 |
+
|
| 13 |
+
APP_NAME = "vivamais-v3-dataset-volume-restore"
|
| 14 |
+
DATA_VOLUME_NAME = "minicpm5-vivamais-v3-rio-data"
|
| 15 |
+
DATASET_REPO = "marinarosa/minicpm5-vivamais-text-sft-v3"
|
| 16 |
+
|
| 17 |
+
RESTORE_MAP = {
|
| 18 |
+
"data/train.jsonl": Path("/data/minicpm5_v3_rio_sft.jsonl"),
|
| 19 |
+
"data/eval/vivamais_qa_eval.jsonl": Path("/data/vivamais_qa_eval.jsonl"),
|
| 20 |
+
"data/teacher/rio31_teacher_distill.jsonl": Path("/data/teacher_distill_v3_rio.jsonl"),
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
app = modal.App(APP_NAME)
|
| 24 |
+
data_volume = modal.Volume.from_name(DATA_VOLUME_NAME, create_if_missing=False)
|
| 25 |
+
|
| 26 |
+
image = modal.Image.debian_slim(python_version="3.11").pip_install("huggingface_hub")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _count_jsonl(path: Path) -> int:
|
| 30 |
+
with path.open(encoding="utf-8") as handle:
|
| 31 |
+
return sum(1 for line in handle if line.strip())
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@app.function(
|
| 35 |
+
image=image,
|
| 36 |
+
volumes={"/data": data_volume},
|
| 37 |
+
timeout=30 * 60,
|
| 38 |
+
cpu=2,
|
| 39 |
+
memory=4096,
|
| 40 |
+
)
|
| 41 |
+
def restore_dataset(repo_id: str, hf_token: str) -> dict[str, Any]:
|
| 42 |
+
from huggingface_hub import hf_hub_download
|
| 43 |
+
|
| 44 |
+
token = hf_token.strip()
|
| 45 |
+
if not token:
|
| 46 |
+
raise RuntimeError("HF_TOKEN is required to restore the v3 data volume")
|
| 47 |
+
|
| 48 |
+
restored: dict[str, Any] = {}
|
| 49 |
+
for source_path, target_path in RESTORE_MAP.items():
|
| 50 |
+
downloaded = Path(
|
| 51 |
+
hf_hub_download(
|
| 52 |
+
repo_id=repo_id,
|
| 53 |
+
filename=source_path,
|
| 54 |
+
repo_type="dataset",
|
| 55 |
+
token=token,
|
| 56 |
+
)
|
| 57 |
+
)
|
| 58 |
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
| 59 |
+
shutil.copy2(downloaded, target_path)
|
| 60 |
+
restored[source_path] = {
|
| 61 |
+
"target": str(target_path),
|
| 62 |
+
"rows": _count_jsonl(target_path),
|
| 63 |
+
"bytes": target_path.stat().st_size,
|
| 64 |
+
}
|
| 65 |
+
data_volume.commit()
|
| 66 |
+
print(json.dumps(restored, ensure_ascii=False, indent=2))
|
| 67 |
+
return restored
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@app.local_entrypoint()
|
| 71 |
+
def main(repo_id: str = DATASET_REPO) -> None:
|
| 72 |
+
result = restore_dataset.remote(
|
| 73 |
+
repo_id=repo_id,
|
| 74 |
+
hf_token=os.environ.get("HF_TOKEN", ""),
|
| 75 |
+
)
|
| 76 |
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|