tiena2cva commited on
Commit
ad20540
·
1 Parent(s): 7ba1307

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 CHANGED
@@ -33,7 +33,7 @@ jobs:
33
 
34
  - name: Restore virtual environment
35
  id: cache-venv
36
- uses: actions/cache@v4
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=Qwen/Qwen3-14B
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": "Qwen/Qwen3-14B",
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": 4096
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: `Qwen/Qwen3-14B`
 
 
 
 
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=Qwen/Qwen3-14B
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 14, 2026.
4
 
5
  ## Summary
6
 
@@ -11,7 +11,7 @@ in the app.
11
 
12
  The current training stack is:
13
 
14
- - Base model: `Qwen/Qwen3-14B`
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
- - `Qwen/Qwen3-14B`
125
 
126
  This is used in:
127
 
128
  - `configs/coach_summary_lora.default.json`
129
  - `scripts/train_coach_summary_lora.py`
130
- - runtime default in `src/pozify/slm/providers.py`
131
 
132
  ### Default training hyperparameters
133
 
134
  | Hyperparameter | Value |
135
  | --- | ---: |
136
- | Base model | `Qwen/Qwen3-14B` |
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 | 4096 |
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
- - fine-tunes with TRL `SFTTrainer`
 
 
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 14, 2026.
4
 
5
  ## Summary
6
 
@@ -11,7 +11,7 @@ in the app.
11
 
12
  The current training stack is:
13
 
14
- - Base model: `Qwen/Qwen3-14B`
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
- - `Qwen/Qwen3-14B`
125
 
126
  This is used in:
127
 
128
  - `configs/coach_summary_lora.default.json`
129
  - `scripts/train_coach_summary_lora.py`
130
- - runtime default in `src/pozify/slm/providers.py`
131
 
132
  ### Default training hyperparameters
133
 
134
  | Hyperparameter | Value |
135
  | --- | ---: |
136
- | Base model | `Qwen/Qwen3-14B` |
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 | 4096 |
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
- - fine-tunes with TRL `SFTTrainer`
 
 
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="A10G",
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
- model = AutoModelForCausalLM.from_pretrained(
481
- str(config["base_model"]),
482
- quantization_config=quantization_config,
483
- device_map="auto",
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
- sft_config_kwargs = {
 
 
 
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(SFTConfig.__init__, "eval_strategy"):
516
- sft_config_kwargs["eval_strategy"] = "epoch"
517
- elif _supports_kwarg(SFTConfig.__init__, "evaluation_strategy"):
518
- sft_config_kwargs["evaluation_strategy"] = "epoch"
519
- if _supports_kwarg(SFTConfig.__init__, "max_seq_length"):
520
- sft_config_kwargs["max_seq_length"] = int(config["max_seq_length"])
521
-
522
- trainer_kwargs: dict[str, Any] = {
523
- "model": model,
524
- "train_dataset": train_dataset,
525
- "eval_dataset": eval_dataset,
526
- "peft_config": peft_config,
527
- "args": SFTConfig(**_filtered_kwargs(SFTConfig.__init__, sft_config_kwargs)),
528
- }
529
- if _supports_kwarg(SFTTrainer.__init__, "processing_class"):
530
- trainer_kwargs["processing_class"] = tokenizer
531
- elif _supports_kwarg(SFTTrainer.__init__, "tokenizer"):
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="A10G",
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="A10G",
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
- torch_dtype=torch.bfloat16,
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": "Qwen/Qwen3-14B",
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": 4096,
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
- torch_dtype="auto",
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(frame, (40 + offset, 80), (220 + offset, 300), (245, 245, 245), -1)
55
- cv2.line(frame, (0, frame_index % height), (width - 1, height - 1), (10, 10, 10), 3)
 
 
 
 
 
 
 
 
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("short_low.mp4", fps=10.0, duration_sec=2.0, size=(320, 240))
 
 
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