Spaces:
Running on Zero
Running on Zero
chore: update base model to NVIDIA-Nemotron-3-Nano-4B-BF16 across configurations and documentation; adjust max sequence length to 2048; upgrade GitHub Actions cache to v5
Browse files- .github/workflows/ci.yml +1 -1
- README.md +5 -1
- configs/coach_summary_lora.default.json +2 -2
- data/sft/coach_summary_eval.jsonl +0 -0
- data/sft/coach_summary_train.jsonl +0 -0
- docs/30-coach-modal-training.md +6 -2
- docs/31-coach-training-report.md +10 -8
- docs/coach-summary-training-report.md +10 -8
- scripts/coach_summary_modal.py +106 -43
- scripts/train_coach_summary_lora.py +2 -2
- src/pozify/slm/providers.py +1 -1
- tests/test_video_qc.py +13 -14
.github/workflows/ci.yml
CHANGED
|
@@ -33,7 +33,7 @@ jobs:
|
|
| 33 |
|
| 34 |
- name: Restore virtual environment
|
| 35 |
id: cache-venv
|
| 36 |
-
uses: actions/cache@
|
| 37 |
with:
|
| 38 |
path: .venv
|
| 39 |
key: venv-${{ runner.os }}-py3.10-${{ hashFiles('uv.lock') }}
|
|
|
|
| 33 |
|
| 34 |
- name: Restore virtual environment
|
| 35 |
id: cache-venv
|
| 36 |
+
uses: actions/cache@v5
|
| 37 |
with:
|
| 38 |
path: .venv
|
| 39 |
key: venv-${{ runner.os }}-py3.10-${{ hashFiles('uv.lock') }}
|
README.md
CHANGED
|
@@ -267,7 +267,7 @@ Download the merged repo locally, then point Pozify at it:
|
|
| 267 |
|
| 268 |
```bash
|
| 269 |
export POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR=/absolute/path/to/merged_model
|
| 270 |
-
export POZIFY_COACH_SUMMARY_BASE_MODEL=
|
| 271 |
export POZIFY_COACH_SUMMARY_ADAPTER_ID=build-small-hackathon/pozify-coach-summary1
|
| 272 |
uv run python app.py
|
| 273 |
```
|
|
@@ -363,6 +363,10 @@ uv run modal run scripts/coach_summary_modal.py \
|
|
| 363 |
--repo-id build-small-hackathon/pozify-coach-summary1
|
| 364 |
```
|
| 365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
Step-by-step:
|
| 367 |
|
| 368 |
```bash
|
|
|
|
| 267 |
|
| 268 |
```bash
|
| 269 |
export POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR=/absolute/path/to/merged_model
|
| 270 |
+
export POZIFY_COACH_SUMMARY_BASE_MODEL=nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16
|
| 271 |
export POZIFY_COACH_SUMMARY_ADAPTER_ID=build-small-hackathon/pozify-coach-summary1
|
| 272 |
uv run python app.py
|
| 273 |
```
|
|
|
|
| 363 |
--repo-id build-small-hackathon/pozify-coach-summary1
|
| 364 |
```
|
| 365 |
|
| 366 |
+
The checked-in fine-tune config uses `nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16` as the base model.
|
| 367 |
+
The Modal training, evaluation, and merge stages request an `A100-80GB` GPU because the Nemotron
|
| 368 |
+
base model can run out of CUDA memory on the previous `A10G` setting.
|
| 369 |
+
|
| 370 |
Step-by-step:
|
| 371 |
|
| 372 |
```bash
|
configs/coach_summary_lora.default.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"base_model": "
|
| 3 |
"train_file": "/Users/h/Documents/Pozify/data/sft/coach_summary_train.jsonl",
|
| 4 |
"eval_file": "/Users/h/Documents/Pozify/data/sft/coach_summary_eval.jsonl",
|
| 5 |
"output_dir": "/Users/h/Documents/Pozify/models/coach_summary_lora",
|
|
@@ -10,5 +10,5 @@
|
|
| 10 |
"num_train_epochs": 2,
|
| 11 |
"per_device_train_batch_size": 1,
|
| 12 |
"gradient_accumulation_steps": 8,
|
| 13 |
-
"max_seq_length":
|
| 14 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"base_model": "nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16",
|
| 3 |
"train_file": "/Users/h/Documents/Pozify/data/sft/coach_summary_train.jsonl",
|
| 4 |
"eval_file": "/Users/h/Documents/Pozify/data/sft/coach_summary_eval.jsonl",
|
| 5 |
"output_dir": "/Users/h/Documents/Pozify/models/coach_summary_lora",
|
|
|
|
| 10 |
"num_train_epochs": 2,
|
| 11 |
"per_device_train_batch_size": 1,
|
| 12 |
"gradient_accumulation_steps": 8,
|
| 13 |
+
"max_seq_length": 2048
|
| 14 |
}
|
data/sft/coach_summary_eval.jsonl
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/sft/coach_summary_train.jsonl
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
docs/30-coach-modal-training.md
CHANGED
|
@@ -68,7 +68,11 @@ The pipeline expects these files:
|
|
| 68 |
|
| 69 |
The default checked-in config currently points to:
|
| 70 |
|
| 71 |
-
- base model: `
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
## Recommended Training Flow
|
| 74 |
|
|
@@ -156,7 +160,7 @@ Download the merged repo or copy the merged directory locally, then:
|
|
| 156 |
|
| 157 |
```bash
|
| 158 |
export POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR=/path/to/merged_model
|
| 159 |
-
export POZIFY_COACH_SUMMARY_BASE_MODEL=
|
| 160 |
export POZIFY_COACH_SUMMARY_ADAPTER_ID=build-small-hackathon/pozify-coach-summary1
|
| 161 |
uv run python app.py
|
| 162 |
```
|
|
|
|
| 68 |
|
| 69 |
The default checked-in config currently points to:
|
| 70 |
|
| 71 |
+
- base model: `nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16`
|
| 72 |
+
|
| 73 |
+
The GPU-backed Modal stages currently request `A100-80GB` for training, evaluation, and merging.
|
| 74 |
+
This is intentional for the Nemotron base model; the previous `A10G` setting can run out of CUDA
|
| 75 |
+
memory during fine-tuning or merge.
|
| 76 |
|
| 77 |
## Recommended Training Flow
|
| 78 |
|
|
|
|
| 160 |
|
| 161 |
```bash
|
| 162 |
export POZIFY_COACH_SUMMARY_LOCAL_MODEL_DIR=/path/to/merged_model
|
| 163 |
+
export POZIFY_COACH_SUMMARY_BASE_MODEL=nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16
|
| 164 |
export POZIFY_COACH_SUMMARY_ADAPTER_ID=build-small-hackathon/pozify-coach-summary1
|
| 165 |
uv run python app.py
|
| 166 |
```
|
docs/31-coach-training-report.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Coach Summary Training Report
|
| 2 |
|
| 3 |
-
Generated from the current Pozify codebase state on June
|
| 4 |
|
| 5 |
## Summary
|
| 6 |
|
|
@@ -11,7 +11,7 @@ in the app.
|
|
| 11 |
|
| 12 |
The current training stack is:
|
| 13 |
|
| 14 |
-
- Base model: `
|
| 15 |
- Fine-tuning method: LoRA / QLoRA-style SFT on Modal
|
| 16 |
- Training target: Pozify-native structured summary generation
|
| 17 |
- Runtime fallback: deterministic conservative summary when model generation fails or verification fails
|
|
@@ -121,19 +121,19 @@ structured.
|
|
| 121 |
|
| 122 |
The codebase now defaults to:
|
| 123 |
|
| 124 |
-
- `
|
| 125 |
|
| 126 |
This is used in:
|
| 127 |
|
| 128 |
- `configs/coach_summary_lora.default.json`
|
| 129 |
- `scripts/train_coach_summary_lora.py`
|
| 130 |
-
-
|
| 131 |
|
| 132 |
### Default training hyperparameters
|
| 133 |
|
| 134 |
| Hyperparameter | Value |
|
| 135 |
| --- | ---: |
|
| 136 |
-
| Base model | `
|
| 137 |
| LoRA rank | 16 |
|
| 138 |
| LoRA alpha | 32 |
|
| 139 |
| LoRA dropout | 0.05 |
|
|
@@ -141,7 +141,7 @@ This is used in:
|
|
| 141 |
| Epochs | 2 |
|
| 142 |
| Batch size per device | 1 |
|
| 143 |
| Gradient accumulation | 8 |
|
| 144 |
-
| Max sequence length |
|
| 145 |
| Default style weight | 0.2 |
|
| 146 |
|
| 147 |
### Modal implementation
|
|
@@ -159,7 +159,9 @@ The full training pipeline is implemented in `scripts/coach_summary_modal.py` wi
|
|
| 159 |
The training stage:
|
| 160 |
|
| 161 |
- loads the base model with 4-bit quantization
|
| 162 |
-
-
|
|
|
|
|
|
|
| 163 |
- saves adapter weights and tokenizer to the Modal model volume
|
| 164 |
|
| 165 |
The merge stage:
|
|
@@ -283,7 +285,7 @@ The app runtime defaults to the fine-tuned coach-summary model:
|
|
| 283 |
|
| 284 |
The deterministic fallback summary remains enabled because hosted inference can still be
|
| 285 |
unavailable, reject a model route, or return output that fails schema validation. If needed,
|
| 286 |
-
`Qwen/Qwen3-14B` can still be used as an explicit base-model override.
|
| 287 |
|
| 288 |
## Assessment
|
| 289 |
|
|
|
|
| 1 |
# Coach Summary Training Report
|
| 2 |
|
| 3 |
+
Generated from the current Pozify codebase state on June 15, 2026.
|
| 4 |
|
| 5 |
## Summary
|
| 6 |
|
|
|
|
| 11 |
|
| 12 |
The current training stack is:
|
| 13 |
|
| 14 |
+
- Base model: `nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16`
|
| 15 |
- Fine-tuning method: LoRA / QLoRA-style SFT on Modal
|
| 16 |
- Training target: Pozify-native structured summary generation
|
| 17 |
- Runtime fallback: deterministic conservative summary when model generation fails or verification fails
|
|
|
|
| 121 |
|
| 122 |
The codebase now defaults to:
|
| 123 |
|
| 124 |
+
- `nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16`
|
| 125 |
|
| 126 |
This is used in:
|
| 127 |
|
| 128 |
- `configs/coach_summary_lora.default.json`
|
| 129 |
- `scripts/train_coach_summary_lora.py`
|
| 130 |
+
- Modal training, evaluation, and merge stages in `scripts/coach_summary_modal.py`
|
| 131 |
|
| 132 |
### Default training hyperparameters
|
| 133 |
|
| 134 |
| Hyperparameter | Value |
|
| 135 |
| --- | ---: |
|
| 136 |
+
| Base model | `nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16` |
|
| 137 |
| LoRA rank | 16 |
|
| 138 |
| LoRA alpha | 32 |
|
| 139 |
| LoRA dropout | 0.05 |
|
|
|
|
| 141 |
| Epochs | 2 |
|
| 142 |
| Batch size per device | 1 |
|
| 143 |
| Gradient accumulation | 8 |
|
| 144 |
+
| Max sequence length | 2048 |
|
| 145 |
| Default style weight | 0.2 |
|
| 146 |
|
| 147 |
### Modal implementation
|
|
|
|
| 159 |
The training stage:
|
| 160 |
|
| 161 |
- loads the base model with 4-bit quantization
|
| 162 |
+
- tokenizes and truncates rows explicitly before training so long JSON evidence rows cannot bypass
|
| 163 |
+
the sequence-length cap
|
| 164 |
+
- fine-tunes the LoRA adapter with the Transformers `Trainer`
|
| 165 |
- saves adapter weights and tokenizer to the Modal model volume
|
| 166 |
|
| 167 |
The merge stage:
|
|
|
|
| 285 |
|
| 286 |
The deterministic fallback summary remains enabled because hosted inference can still be
|
| 287 |
unavailable, reject a model route, or return output that fails schema validation. If needed,
|
| 288 |
+
`Qwen/Qwen3-14B` can still be used as an explicit previous-base-model override.
|
| 289 |
|
| 290 |
## Assessment
|
| 291 |
|
docs/coach-summary-training-report.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Coach Summary Training Report
|
| 2 |
|
| 3 |
-
Generated from the current Pozify codebase state on June
|
| 4 |
|
| 5 |
## Summary
|
| 6 |
|
|
@@ -11,7 +11,7 @@ in the app.
|
|
| 11 |
|
| 12 |
The current training stack is:
|
| 13 |
|
| 14 |
-
- Base model: `
|
| 15 |
- Fine-tuning method: LoRA / QLoRA-style SFT on Modal
|
| 16 |
- Training target: Pozify-native structured summary generation
|
| 17 |
- Runtime fallback: deterministic conservative summary when model generation fails or verification fails
|
|
@@ -121,19 +121,19 @@ structured.
|
|
| 121 |
|
| 122 |
The codebase now defaults to:
|
| 123 |
|
| 124 |
-
- `
|
| 125 |
|
| 126 |
This is used in:
|
| 127 |
|
| 128 |
- `configs/coach_summary_lora.default.json`
|
| 129 |
- `scripts/train_coach_summary_lora.py`
|
| 130 |
-
-
|
| 131 |
|
| 132 |
### Default training hyperparameters
|
| 133 |
|
| 134 |
| Hyperparameter | Value |
|
| 135 |
| --- | ---: |
|
| 136 |
-
| Base model | `
|
| 137 |
| LoRA rank | 16 |
|
| 138 |
| LoRA alpha | 32 |
|
| 139 |
| LoRA dropout | 0.05 |
|
|
@@ -141,7 +141,7 @@ This is used in:
|
|
| 141 |
| Epochs | 2 |
|
| 142 |
| Batch size per device | 1 |
|
| 143 |
| Gradient accumulation | 8 |
|
| 144 |
-
| Max sequence length |
|
| 145 |
| Default style weight | 0.2 |
|
| 146 |
|
| 147 |
### Modal implementation
|
|
@@ -159,7 +159,9 @@ The full training pipeline is implemented in `scripts/coach_summary_modal.py` wi
|
|
| 159 |
The training stage:
|
| 160 |
|
| 161 |
- loads the base model with 4-bit quantization
|
| 162 |
-
-
|
|
|
|
|
|
|
| 163 |
- saves adapter weights and tokenizer to the Modal model volume
|
| 164 |
|
| 165 |
The merge stage:
|
|
@@ -284,7 +286,7 @@ The app runtime defaults to the fine-tuned coach-summary model:
|
|
| 284 |
|
| 285 |
The deterministic fallback summary remains enabled because hosted inference can still be
|
| 286 |
unavailable, reject a model route, or return output that fails schema validation. If needed,
|
| 287 |
-
`Qwen/Qwen3-14B` can still be used as an explicit base-model override.
|
| 288 |
|
| 289 |
## Assessment
|
| 290 |
|
|
|
|
| 1 |
# Coach Summary Training Report
|
| 2 |
|
| 3 |
+
Generated from the current Pozify codebase state on June 15, 2026.
|
| 4 |
|
| 5 |
## Summary
|
| 6 |
|
|
|
|
| 11 |
|
| 12 |
The current training stack is:
|
| 13 |
|
| 14 |
+
- Base model: `nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16`
|
| 15 |
- Fine-tuning method: LoRA / QLoRA-style SFT on Modal
|
| 16 |
- Training target: Pozify-native structured summary generation
|
| 17 |
- Runtime fallback: deterministic conservative summary when model generation fails or verification fails
|
|
|
|
| 121 |
|
| 122 |
The codebase now defaults to:
|
| 123 |
|
| 124 |
+
- `nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16`
|
| 125 |
|
| 126 |
This is used in:
|
| 127 |
|
| 128 |
- `configs/coach_summary_lora.default.json`
|
| 129 |
- `scripts/train_coach_summary_lora.py`
|
| 130 |
+
- Modal training, evaluation, and merge stages in `scripts/coach_summary_modal.py`
|
| 131 |
|
| 132 |
### Default training hyperparameters
|
| 133 |
|
| 134 |
| Hyperparameter | Value |
|
| 135 |
| --- | ---: |
|
| 136 |
+
| Base model | `nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16` |
|
| 137 |
| LoRA rank | 16 |
|
| 138 |
| LoRA alpha | 32 |
|
| 139 |
| LoRA dropout | 0.05 |
|
|
|
|
| 141 |
| Epochs | 2 |
|
| 142 |
| Batch size per device | 1 |
|
| 143 |
| Gradient accumulation | 8 |
|
| 144 |
+
| Max sequence length | 2048 |
|
| 145 |
| Default style weight | 0.2 |
|
| 146 |
|
| 147 |
### Modal implementation
|
|
|
|
| 159 |
The training stage:
|
| 160 |
|
| 161 |
- loads the base model with 4-bit quantization
|
| 162 |
+
- tokenizes and truncates rows explicitly before training so long JSON evidence rows cannot bypass
|
| 163 |
+
the sequence-length cap
|
| 164 |
+
- fine-tunes the LoRA adapter with the Transformers `Trainer`
|
| 165 |
- saves adapter weights and tokenizer to the Modal model volume
|
| 166 |
|
| 167 |
The merge stage:
|
|
|
|
| 286 |
|
| 287 |
The deterministic fallback summary remains enabled because hosted inference can still be
|
| 288 |
unavailable, reject a model route, or return output that fails schema validation. If needed,
|
| 289 |
+
`Qwen/Qwen3-14B` can still be used as an explicit previous-base-model override.
|
| 290 |
|
| 291 |
## Assessment
|
| 292 |
|
scripts/coach_summary_modal.py
CHANGED
|
@@ -45,6 +45,7 @@ HF_DATA_FILENAMES = (
|
|
| 45 |
"coach_summary_eval.jsonl",
|
| 46 |
"public_fitness_style.jsonl",
|
| 47 |
)
|
|
|
|
| 48 |
|
| 49 |
image = (
|
| 50 |
modal.Image.debian_slim(python_version="3.10")
|
|
@@ -165,9 +166,9 @@ def _filtered_kwargs(callable_obj: Any, kwargs: dict[str, Any]) -> dict[str, Any
|
|
| 165 |
return {key: value for key, value in kwargs.items() if key in parameters}
|
| 166 |
|
| 167 |
|
| 168 |
-
def _load_config() -> dict[str, Any]:
|
| 169 |
config = _read_json(DEFAULT_CONFIG_PATH)
|
| 170 |
-
if TRAINING_CONFIG_PATH.exists():
|
| 171 |
config.update(_read_json(TRAINING_CONFIG_PATH))
|
| 172 |
return config
|
| 173 |
|
|
@@ -419,7 +420,7 @@ def prepare_data() -> dict[str, Any]:
|
|
| 419 |
|
| 420 |
|
| 421 |
@app.function(
|
| 422 |
-
gpu=
|
| 423 |
volumes={str(DATA_ROOT): data_volume, str(MODEL_ROOT): model_volume},
|
| 424 |
secrets=[_hf_secret()],
|
| 425 |
timeout=3 * 60 * 60,
|
|
@@ -429,17 +430,20 @@ def train(
|
|
| 429 |
style_weight: float = 0.2,
|
| 430 |
output_subdir: str = "adapter",
|
| 431 |
) -> dict[str, Any]:
|
|
|
|
| 432 |
import torch
|
| 433 |
from datasets import Dataset
|
| 434 |
-
from peft import LoraConfig
|
| 435 |
from transformers import (
|
| 436 |
AutoModelForCausalLM,
|
| 437 |
AutoTokenizer,
|
| 438 |
BitsAndBytesConfig,
|
|
|
|
|
|
|
|
|
|
| 439 |
)
|
| 440 |
-
from trl import SFTConfig, SFTTrainer
|
| 441 |
|
| 442 |
-
config = _load_config()
|
| 443 |
if epochs is not None:
|
| 444 |
config["num_train_epochs"] = epochs
|
| 445 |
config["style_weight"] = style_weight
|
|
@@ -463,13 +467,51 @@ def train(
|
|
| 463 |
style_weight=style_weight,
|
| 464 |
)
|
| 465 |
eval_dataset_rows = _build_eval_dataset_rows(eval_rows)
|
| 466 |
-
train_dataset = Dataset.from_list(training_rows)
|
| 467 |
-
eval_dataset = Dataset.from_list(eval_dataset_rows)
|
| 468 |
|
| 469 |
tokenizer = AutoTokenizer.from_pretrained(str(config["base_model"]))
|
| 470 |
if tokenizer.pad_token is None:
|
| 471 |
tokenizer.pad_token = tokenizer.eos_token
|
| 472 |
tokenizer.padding_side = "right"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
|
| 474 |
quantization_config = BitsAndBytesConfig(
|
| 475 |
load_in_4bit=True,
|
|
@@ -477,11 +519,24 @@ def train(
|
|
| 477 |
bnb_4bit_quant_type="nf4",
|
| 478 |
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 479 |
)
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
device_map
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
peft_config = LoraConfig(
|
| 487 |
r=int(config.get("lora_r", 16)),
|
|
@@ -500,44 +555,49 @@ def train(
|
|
| 500 |
],
|
| 501 |
)
|
| 502 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
adapter_dir = MODEL_ROOT / output_subdir
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
| 505 |
"output_dir": str(adapter_dir),
|
| 506 |
"learning_rate": float(config["learning_rate"]),
|
| 507 |
"num_train_epochs": float(config["num_train_epochs"]),
|
| 508 |
"per_device_train_batch_size": int(config["per_device_train_batch_size"]),
|
|
|
|
| 509 |
"gradient_accumulation_steps": int(config["gradient_accumulation_steps"]),
|
| 510 |
"save_strategy": "epoch",
|
| 511 |
"logging_steps": 10,
|
| 512 |
"bf16": True,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
"report_to": [],
|
| 514 |
}
|
| 515 |
-
if _supports_kwarg(
|
| 516 |
-
|
| 517 |
-
elif _supports_kwarg(
|
| 518 |
-
|
| 519 |
-
if _supports_kwarg(
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
trainer_kwargs["tokenizer"] = tokenizer
|
| 533 |
-
if _supports_kwarg(SFTTrainer.__init__, "dataset_text_field"):
|
| 534 |
-
trainer_kwargs["dataset_text_field"] = "text"
|
| 535 |
-
elif _supports_kwarg(SFTTrainer.__init__, "formatting_func"):
|
| 536 |
-
trainer_kwargs["formatting_func"] = lambda example: example["text"]
|
| 537 |
-
if _supports_kwarg(SFTTrainer.__init__, "max_seq_length"):
|
| 538 |
-
trainer_kwargs["max_seq_length"] = int(config["max_seq_length"])
|
| 539 |
-
|
| 540 |
-
trainer = SFTTrainer(**_filtered_kwargs(SFTTrainer.__init__, trainer_kwargs))
|
| 541 |
train_result = trainer.train()
|
| 542 |
trainer.save_model(str(adapter_dir))
|
| 543 |
tokenizer.save_pretrained(str(adapter_dir))
|
|
@@ -555,6 +615,8 @@ def train(
|
|
| 555 |
"global_step": int(getattr(train_result, "global_step", 0)),
|
| 556 |
"training_loss": float(getattr(train_result, "training_loss", 0.0)),
|
| 557 |
"output_dir": str(adapter_dir),
|
|
|
|
|
|
|
| 558 |
}
|
| 559 |
_write_json(TRAINING_CONFIG_PATH, config)
|
| 560 |
_write_json(TRAINING_SUMMARY_PATH, summary)
|
|
@@ -563,7 +625,7 @@ def train(
|
|
| 563 |
|
| 564 |
|
| 565 |
@app.function(
|
| 566 |
-
gpu=
|
| 567 |
volumes={str(DATA_ROOT): data_volume, str(MODEL_ROOT): model_volume},
|
| 568 |
secrets=[_hf_secret()],
|
| 569 |
timeout=90 * 60,
|
|
@@ -603,6 +665,7 @@ def evaluate(
|
|
| 603 |
base_model = AutoModelForCausalLM.from_pretrained(
|
| 604 |
str(config["base_model"]),
|
| 605 |
quantization_config=quantization_config,
|
|
|
|
| 606 |
device_map="auto",
|
| 607 |
)
|
| 608 |
model = PeftModel.from_pretrained(base_model, str(adapter_dir))
|
|
@@ -710,7 +773,7 @@ def _upload_hf_file(
|
|
| 710 |
|
| 711 |
|
| 712 |
@app.function(
|
| 713 |
-
gpu=
|
| 714 |
volumes={str(MODEL_ROOT): model_volume},
|
| 715 |
secrets=[_hf_secret()],
|
| 716 |
timeout=90 * 60,
|
|
@@ -739,7 +802,7 @@ def merge(
|
|
| 739 |
tokenizer = AutoTokenizer.from_pretrained(str(adapter_dir))
|
| 740 |
base_model = AutoModelForCausalLM.from_pretrained(
|
| 741 |
str(config["base_model"]),
|
| 742 |
-
|
| 743 |
device_map="auto",
|
| 744 |
low_cpu_mem_usage=True,
|
| 745 |
)
|
|
|
|
| 45 |
"coach_summary_eval.jsonl",
|
| 46 |
"public_fitness_style.jsonl",
|
| 47 |
)
|
| 48 |
+
TRAINING_GPU = "A100-80GB"
|
| 49 |
|
| 50 |
image = (
|
| 51 |
modal.Image.debian_slim(python_version="3.10")
|
|
|
|
| 166 |
return {key: value for key, value in kwargs.items() if key in parameters}
|
| 167 |
|
| 168 |
|
| 169 |
+
def _load_config(*, include_saved_training_config: bool = True) -> dict[str, Any]:
|
| 170 |
config = _read_json(DEFAULT_CONFIG_PATH)
|
| 171 |
+
if include_saved_training_config and TRAINING_CONFIG_PATH.exists():
|
| 172 |
config.update(_read_json(TRAINING_CONFIG_PATH))
|
| 173 |
return config
|
| 174 |
|
|
|
|
| 420 |
|
| 421 |
|
| 422 |
@app.function(
|
| 423 |
+
gpu=TRAINING_GPU,
|
| 424 |
volumes={str(DATA_ROOT): data_volume, str(MODEL_ROOT): model_volume},
|
| 425 |
secrets=[_hf_secret()],
|
| 426 |
timeout=3 * 60 * 60,
|
|
|
|
| 430 |
style_weight: float = 0.2,
|
| 431 |
output_subdir: str = "adapter",
|
| 432 |
) -> dict[str, Any]:
|
| 433 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 434 |
import torch
|
| 435 |
from datasets import Dataset
|
| 436 |
+
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
|
| 437 |
from transformers import (
|
| 438 |
AutoModelForCausalLM,
|
| 439 |
AutoTokenizer,
|
| 440 |
BitsAndBytesConfig,
|
| 441 |
+
DataCollatorForLanguageModeling,
|
| 442 |
+
Trainer,
|
| 443 |
+
TrainingArguments,
|
| 444 |
)
|
|
|
|
| 445 |
|
| 446 |
+
config = _load_config(include_saved_training_config=False)
|
| 447 |
if epochs is not None:
|
| 448 |
config["num_train_epochs"] = epochs
|
| 449 |
config["style_weight"] = style_weight
|
|
|
|
| 467 |
style_weight=style_weight,
|
| 468 |
)
|
| 469 |
eval_dataset_rows = _build_eval_dataset_rows(eval_rows)
|
|
|
|
|
|
|
| 470 |
|
| 471 |
tokenizer = AutoTokenizer.from_pretrained(str(config["base_model"]))
|
| 472 |
if tokenizer.pad_token is None:
|
| 473 |
tokenizer.pad_token = tokenizer.eos_token
|
| 474 |
tokenizer.padding_side = "right"
|
| 475 |
+
max_seq_length = int(config.get("max_seq_length", 2048))
|
| 476 |
+
|
| 477 |
+
def tokenize_rows(rows: list[dict[str, str]]) -> tuple[list[dict[str, list[int]]], dict[str, Any]]:
|
| 478 |
+
tokenized_rows: list[dict[str, list[int]]] = []
|
| 479 |
+
lengths: list[int] = []
|
| 480 |
+
truncated_count = 0
|
| 481 |
+
head_tokens = min(256, max_seq_length // 4)
|
| 482 |
+
tail_tokens = max_seq_length - head_tokens
|
| 483 |
+
for row in rows:
|
| 484 |
+
input_ids = tokenizer(
|
| 485 |
+
str(row["text"]),
|
| 486 |
+
add_special_tokens=False,
|
| 487 |
+
truncation=False,
|
| 488 |
+
)["input_ids"]
|
| 489 |
+
if tokenizer.eos_token_id is not None:
|
| 490 |
+
input_ids = [*input_ids, int(tokenizer.eos_token_id)]
|
| 491 |
+
lengths.append(len(input_ids))
|
| 492 |
+
if len(input_ids) > max_seq_length:
|
| 493 |
+
truncated_count += 1
|
| 494 |
+
input_ids = [*input_ids[:head_tokens], *input_ids[-tail_tokens:]]
|
| 495 |
+
tokenized_rows.append(
|
| 496 |
+
{
|
| 497 |
+
"input_ids": input_ids,
|
| 498 |
+
"attention_mask": [1] * len(input_ids),
|
| 499 |
+
}
|
| 500 |
+
)
|
| 501 |
+
stats = {
|
| 502 |
+
"max_seq_length": max_seq_length,
|
| 503 |
+
"truncated_row_count": truncated_count,
|
| 504 |
+
"max_input_tokens_before_truncation": max(lengths) if lengths else 0,
|
| 505 |
+
"avg_input_tokens_before_truncation": round(sum(lengths) / len(lengths), 2)
|
| 506 |
+
if lengths
|
| 507 |
+
else 0,
|
| 508 |
+
}
|
| 509 |
+
return tokenized_rows, stats
|
| 510 |
+
|
| 511 |
+
tokenized_training_rows, train_token_stats = tokenize_rows(training_rows)
|
| 512 |
+
tokenized_eval_rows, eval_token_stats = tokenize_rows(eval_dataset_rows)
|
| 513 |
+
train_dataset = Dataset.from_list(tokenized_training_rows)
|
| 514 |
+
eval_dataset = Dataset.from_list(tokenized_eval_rows)
|
| 515 |
|
| 516 |
quantization_config = BitsAndBytesConfig(
|
| 517 |
load_in_4bit=True,
|
|
|
|
| 519 |
bnb_4bit_quant_type="nf4",
|
| 520 |
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 521 |
)
|
| 522 |
+
model_kwargs: dict[str, Any] = {
|
| 523 |
+
"quantization_config": quantization_config,
|
| 524 |
+
"dtype": torch.bfloat16,
|
| 525 |
+
"device_map": "auto",
|
| 526 |
+
"attn_implementation": "sdpa",
|
| 527 |
+
}
|
| 528 |
+
try:
|
| 529 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 530 |
+
str(config["base_model"]),
|
| 531 |
+
**model_kwargs,
|
| 532 |
+
)
|
| 533 |
+
except (TypeError, ValueError):
|
| 534 |
+
model_kwargs.pop("attn_implementation", None)
|
| 535 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 536 |
+
str(config["base_model"]),
|
| 537 |
+
**model_kwargs,
|
| 538 |
+
)
|
| 539 |
+
model.config.use_cache = False
|
| 540 |
|
| 541 |
peft_config = LoraConfig(
|
| 542 |
r=int(config.get("lora_r", 16)),
|
|
|
|
| 555 |
],
|
| 556 |
)
|
| 557 |
|
| 558 |
+
prepare_kwargs: dict[str, Any] = {}
|
| 559 |
+
if _supports_kwarg(prepare_model_for_kbit_training, "use_gradient_checkpointing"):
|
| 560 |
+
prepare_kwargs["use_gradient_checkpointing"] = True
|
| 561 |
+
model = prepare_model_for_kbit_training(model, **prepare_kwargs)
|
| 562 |
+
model = get_peft_model(model, peft_config)
|
| 563 |
+
|
| 564 |
adapter_dir = MODEL_ROOT / output_subdir
|
| 565 |
+
if adapter_dir.exists():
|
| 566 |
+
shutil.rmtree(adapter_dir)
|
| 567 |
+
adapter_dir.mkdir(parents=True, exist_ok=True)
|
| 568 |
+
training_args_kwargs = {
|
| 569 |
"output_dir": str(adapter_dir),
|
| 570 |
"learning_rate": float(config["learning_rate"]),
|
| 571 |
"num_train_epochs": float(config["num_train_epochs"]),
|
| 572 |
"per_device_train_batch_size": int(config["per_device_train_batch_size"]),
|
| 573 |
+
"per_device_eval_batch_size": 1,
|
| 574 |
"gradient_accumulation_steps": int(config["gradient_accumulation_steps"]),
|
| 575 |
"save_strategy": "epoch",
|
| 576 |
"logging_steps": 10,
|
| 577 |
"bf16": True,
|
| 578 |
+
"gradient_checkpointing": True,
|
| 579 |
+
"remove_unused_columns": False,
|
| 580 |
+
"prediction_loss_only": True,
|
| 581 |
+
"optim": "paged_adamw_8bit",
|
| 582 |
"report_to": [],
|
| 583 |
}
|
| 584 |
+
if _supports_kwarg(TrainingArguments.__init__, "eval_strategy"):
|
| 585 |
+
training_args_kwargs["eval_strategy"] = "epoch"
|
| 586 |
+
elif _supports_kwarg(TrainingArguments.__init__, "evaluation_strategy"):
|
| 587 |
+
training_args_kwargs["evaluation_strategy"] = "epoch"
|
| 588 |
+
if _supports_kwarg(TrainingArguments.__init__, "gradient_checkpointing_kwargs"):
|
| 589 |
+
training_args_kwargs["gradient_checkpointing_kwargs"] = {"use_reentrant": False}
|
| 590 |
+
|
| 591 |
+
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
|
| 592 |
+
trainer = Trainer(
|
| 593 |
+
model=model,
|
| 594 |
+
train_dataset=train_dataset,
|
| 595 |
+
eval_dataset=eval_dataset,
|
| 596 |
+
args=TrainingArguments(
|
| 597 |
+
**_filtered_kwargs(TrainingArguments.__init__, training_args_kwargs)
|
| 598 |
+
),
|
| 599 |
+
data_collator=data_collator,
|
| 600 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
train_result = trainer.train()
|
| 602 |
trainer.save_model(str(adapter_dir))
|
| 603 |
tokenizer.save_pretrained(str(adapter_dir))
|
|
|
|
| 615 |
"global_step": int(getattr(train_result, "global_step", 0)),
|
| 616 |
"training_loss": float(getattr(train_result, "training_loss", 0.0)),
|
| 617 |
"output_dir": str(adapter_dir),
|
| 618 |
+
"train_token_stats": train_token_stats,
|
| 619 |
+
"eval_token_stats": eval_token_stats,
|
| 620 |
}
|
| 621 |
_write_json(TRAINING_CONFIG_PATH, config)
|
| 622 |
_write_json(TRAINING_SUMMARY_PATH, summary)
|
|
|
|
| 625 |
|
| 626 |
|
| 627 |
@app.function(
|
| 628 |
+
gpu=TRAINING_GPU,
|
| 629 |
volumes={str(DATA_ROOT): data_volume, str(MODEL_ROOT): model_volume},
|
| 630 |
secrets=[_hf_secret()],
|
| 631 |
timeout=90 * 60,
|
|
|
|
| 665 |
base_model = AutoModelForCausalLM.from_pretrained(
|
| 666 |
str(config["base_model"]),
|
| 667 |
quantization_config=quantization_config,
|
| 668 |
+
dtype=torch.bfloat16,
|
| 669 |
device_map="auto",
|
| 670 |
)
|
| 671 |
model = PeftModel.from_pretrained(base_model, str(adapter_dir))
|
|
|
|
| 773 |
|
| 774 |
|
| 775 |
@app.function(
|
| 776 |
+
gpu=TRAINING_GPU,
|
| 777 |
volumes={str(MODEL_ROOT): model_volume},
|
| 778 |
secrets=[_hf_secret()],
|
| 779 |
timeout=90 * 60,
|
|
|
|
| 802 |
tokenizer = AutoTokenizer.from_pretrained(str(adapter_dir))
|
| 803 |
base_model = AutoModelForCausalLM.from_pretrained(
|
| 804 |
str(config["base_model"]),
|
| 805 |
+
dtype=torch.bfloat16,
|
| 806 |
device_map="auto",
|
| 807 |
low_cpu_mem_usage=True,
|
| 808 |
)
|
scripts/train_coach_summary_lora.py
CHANGED
|
@@ -11,7 +11,7 @@ sys.path.insert(0, str(ROOT / "src"))
|
|
| 11 |
|
| 12 |
|
| 13 |
DEFAULT_CONFIG = {
|
| 14 |
-
"base_model": "
|
| 15 |
"train_file": str(ROOT / "data/sft/coach_summary_train.jsonl"),
|
| 16 |
"eval_file": str(ROOT / "data/sft/coach_summary_eval.jsonl"),
|
| 17 |
"output_dir": str(ROOT / "models/coach_summary_lora"),
|
|
@@ -22,7 +22,7 @@ DEFAULT_CONFIG = {
|
|
| 22 |
"num_train_epochs": 2,
|
| 23 |
"per_device_train_batch_size": 1,
|
| 24 |
"gradient_accumulation_steps": 8,
|
| 25 |
-
"max_seq_length":
|
| 26 |
}
|
| 27 |
|
| 28 |
|
|
|
|
| 11 |
|
| 12 |
|
| 13 |
DEFAULT_CONFIG = {
|
| 14 |
+
"base_model": "nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16",
|
| 15 |
"train_file": str(ROOT / "data/sft/coach_summary_train.jsonl"),
|
| 16 |
"eval_file": str(ROOT / "data/sft/coach_summary_eval.jsonl"),
|
| 17 |
"output_dir": str(ROOT / "models/coach_summary_lora"),
|
|
|
|
| 22 |
"num_train_epochs": 2,
|
| 23 |
"per_device_train_batch_size": 1,
|
| 24 |
"gradient_accumulation_steps": 8,
|
| 25 |
+
"max_seq_length": 2048,
|
| 26 |
}
|
| 27 |
|
| 28 |
|
src/pozify/slm/providers.py
CHANGED
|
@@ -210,7 +210,7 @@ def _load_local_transformers_backend(model: str, token: str | None) -> tuple[Any
|
|
| 210 |
language_model = AutoModelForCausalLM.from_pretrained(
|
| 211 |
model,
|
| 212 |
device_map="auto",
|
| 213 |
-
|
| 214 |
token=token,
|
| 215 |
)
|
| 216 |
language_model.eval()
|
|
|
|
| 210 |
language_model = AutoModelForCausalLM.from_pretrained(
|
| 211 |
model,
|
| 212 |
device_map="auto",
|
| 213 |
+
dtype="auto",
|
| 214 |
token=token,
|
| 215 |
)
|
| 216 |
language_model.eval()
|
tests/test_video_qc.py
CHANGED
|
@@ -51,8 +51,16 @@ class VideoQCTests(unittest.TestCase):
|
|
| 51 |
else:
|
| 52 |
frame = np.full((height, width, 3), 135, dtype=np.uint8)
|
| 53 |
offset = frame_index % max(1, width // 4)
|
| 54 |
-
cv2.rectangle(
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
cv2.putText(
|
| 57 |
frame,
|
| 58 |
str(frame_index),
|
|
@@ -85,17 +93,6 @@ class VideoQCTests(unittest.TestCase):
|
|
| 85 |
self.assertIsNotNone(manifest.brightness_mean)
|
| 86 |
self.assertIsNotNone(manifest.blur_laplacian_var)
|
| 87 |
|
| 88 |
-
def test_rotated_mov_reports_display_dimensions(self) -> None:
|
| 89 |
-
path = FIXTURES_DIR / "IMG_2296.MOV"
|
| 90 |
-
self.assertTrue(path.exists(), path)
|
| 91 |
-
|
| 92 |
-
manifest = video_qc.run(str(path))
|
| 93 |
-
|
| 94 |
-
self.assertTrue(manifest.analysis_allowed)
|
| 95 |
-
self.assertEqual(manifest.width, 1080)
|
| 96 |
-
self.assertEqual(manifest.height, 1920)
|
| 97 |
-
self.assertEqual(manifest.quality_warnings, [])
|
| 98 |
-
|
| 99 |
def test_invalid_video_sets_decode_failure_and_blocks_analysis(self) -> None:
|
| 100 |
manifest = video_qc.run(str(Path(self.temp_dir.name) / "missing.mp4"))
|
| 101 |
|
|
@@ -105,7 +102,9 @@ class VideoQCTests(unittest.TestCase):
|
|
| 105 |
self.assertEqual(manifest.sampled_frames, 0)
|
| 106 |
|
| 107 |
def test_short_low_resolution_low_fps_video_reports_warnings(self) -> None:
|
| 108 |
-
path = self._write_video(
|
|
|
|
|
|
|
| 109 |
|
| 110 |
manifest = video_qc.run(str(path))
|
| 111 |
|
|
|
|
| 51 |
else:
|
| 52 |
frame = np.full((height, width, 3), 135, dtype=np.uint8)
|
| 53 |
offset = frame_index % max(1, width // 4)
|
| 54 |
+
cv2.rectangle(
|
| 55 |
+
frame, (40 + offset, 80), (220 + offset, 300), (245, 245, 245), -1
|
| 56 |
+
)
|
| 57 |
+
cv2.line(
|
| 58 |
+
frame,
|
| 59 |
+
(0, frame_index % height),
|
| 60 |
+
(width - 1, height - 1),
|
| 61 |
+
(10, 10, 10),
|
| 62 |
+
3,
|
| 63 |
+
)
|
| 64 |
cv2.putText(
|
| 65 |
frame,
|
| 66 |
str(frame_index),
|
|
|
|
| 93 |
self.assertIsNotNone(manifest.brightness_mean)
|
| 94 |
self.assertIsNotNone(manifest.blur_laplacian_var)
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
def test_invalid_video_sets_decode_failure_and_blocks_analysis(self) -> None:
|
| 97 |
manifest = video_qc.run(str(Path(self.temp_dir.name) / "missing.mp4"))
|
| 98 |
|
|
|
|
| 102 |
self.assertEqual(manifest.sampled_frames, 0)
|
| 103 |
|
| 104 |
def test_short_low_resolution_low_fps_video_reports_warnings(self) -> None:
|
| 105 |
+
path = self._write_video(
|
| 106 |
+
"short_low.mp4", fps=10.0, duration_sec=2.0, size=(320, 240)
|
| 107 |
+
)
|
| 108 |
|
| 109 |
manifest = video_qc.run(str(path))
|
| 110 |
|