trainsight-preflight 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
trainsight/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ """
2
+ TrainSight: Preflight Checks for LLM Training Inputs and Loss Contracts.
3
+ """
4
+
5
+ __version__ = "0.2.0"
6
+
7
+ from trainsight.core.contracts import LossContract
8
+ from trainsight.core.exceptions import (
9
+ TrainSightError,
10
+ ContractViolationError,
11
+ PreflightQualityGateError,
12
+ )
13
+ from trainsight.core.report import AuditReport, RowDiagnosis, TensorEvidence
14
+ from trainsight.inspectors.sft_inspector import SFTInspector, SFTReport
15
+ from trainsight.inspectors.dpo_inspector import DPOInspector, DPOReport
16
+ from trainsight.preprocessing.sft_preprocessor import SFTPreprocessor, SFTExample
17
+ from trainsight.preprocessing.dpo_preprocessor import DPOPreprocessor, DPOExample
18
+ from trainsight.reconcilers.sft_reconciler import SFTReconciler
19
+ from trainsight.reconcilers.callbacks import TrainSightCallback
20
+
21
+ __all__ = [
22
+ "LossContract",
23
+ "TrainSightError",
24
+ "ContractViolationError",
25
+ "PreflightQualityGateError",
26
+ "AuditReport",
27
+ "RowDiagnosis",
28
+ "TensorEvidence",
29
+ "SFTInspector",
30
+ "SFTReport",
31
+ "DPOInspector",
32
+ "DPOReport",
33
+ "SFTPreprocessor",
34
+ "SFTExample",
35
+ "DPOPreprocessor",
36
+ "DPOExample",
37
+ "SFTReconciler",
38
+ "TrainSightCallback",
39
+ ]
@@ -0,0 +1,3 @@
1
+ from trainsight.cli.main import app
2
+
3
+ __all__ = ["app"]
@@ -0,0 +1,80 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+ import typer
4
+ from rich.table import Table
5
+
6
+ from trainsight.inspectors.sft_inspector import SFTInspector
7
+ from trainsight.inspectors.dpo_inspector import DPOInspector
8
+ from trainsight.utils.logging import get_console
9
+
10
+ app = typer.Typer(help="Inspect raw training datasets on disk before GPU allocation.")
11
+
12
+ @app.callback(invoke_without_command=True)
13
+ def check(
14
+ file_path: Path = typer.Argument(..., help="Path to JSONL dataset file"),
15
+ task: str = typer.Option("sft", "--task", "-t", help="Task type: sft or dpo"),
16
+ tokenizer: str = typer.Option("Qwen/Qwen2.5-0.5B-Instruct", "--tokenizer", "-m", help="Tokenizer name or path"),
17
+ max_seq_len: int = typer.Option(2048, "--max-seq-len", "-l", help="Max sequence length"),
18
+ strict: bool = typer.Option(True, "--strict/--no-strict", help="Fail with non-zero exit code on contract violations"),
19
+ ):
20
+ console = get_console()
21
+ if not file_path.exists():
22
+ console.print(f"[bold red]Error:[/] File not found: {file_path}")
23
+ raise typer.Exit(code=1)
24
+
25
+ console.print(f"\n[bold cyan]TrainSight Preflight Inspection[/]")
26
+ console.print(f"File: [bold]{file_path.name}[/] | Task: [bold]{task.upper()}[/] | Max Length: {max_seq_len}\n")
27
+
28
+ if task.lower() == "sft":
29
+ inspector = SFTInspector(
30
+ tokenizer_name=tokenizer,
31
+ max_seq_len_threshold=max_seq_len,
32
+ strict=strict,
33
+ )
34
+ report = inspector.inspect_file(file_path)
35
+
36
+ table = Table(title="SFT Preflight Summary", show_lines=True)
37
+ table.add_column("Metric", style="cyan")
38
+ table.add_column("Value", justify="right")
39
+ table.add_column("Status", justify="left")
40
+
41
+ table.add_row("Total Samples", str(report.total_samples), "[green]OK[/]")
42
+ table.add_row("Valid Samples", str(report.valid_samples_count), "[green]OK[/]")
43
+
44
+ status_empty = "[green]OK[/]" if report.empty_completion_count == 0 else "[bold red]FAIL[/]"
45
+ table.add_row("Empty Completions", str(report.empty_completion_count), status_empty)
46
+
47
+ status_trunc = "[green]OK[/]" if report.truncated_answers_count == 0 else "[bold red]FAIL[/]"
48
+ table.add_row("Truncated Answers", str(report.truncated_answers_count), status_trunc)
49
+
50
+ status_mask = "[green]OK[/]" if report.loss_mask_misaligned_count == 0 else "[bold red]FAIL[/]"
51
+ table.add_row("Misaligned Loss Masks", str(report.loss_mask_misaligned_count), status_mask)
52
+
53
+ table.add_row("Mean Sequence Length", f"{report.avg_seq_len:.1f} tokens", "[dim]Info[/]")
54
+ table.add_row("Max Sequence Length", f"{report.max_seq_len} tokens", "[dim]Info[/]")
55
+
56
+ console.print(table)
57
+
58
+ passed = (
59
+ report.invalid_rows_count == 0
60
+ and report.empty_completion_count == 0
61
+ and report.truncated_answers_count == 0
62
+ and report.loss_mask_misaligned_count == 0
63
+ )
64
+
65
+ if passed:
66
+ console.print("\n[bold green]✓ Preflight Quality Gate Passed:[/] Dataset meets all loss contract requirements.\n")
67
+ else:
68
+ console.print("\n[bold red]✗ Preflight Quality Gate Failed:[/] Contract violations detected above.\n")
69
+ if strict:
70
+ raise typer.Exit(code=1)
71
+
72
+ elif task.lower() == "dpo":
73
+ inspector = DPOInspector(
74
+ tokenizer_name=tokenizer,
75
+ max_seq_len_threshold=max_seq_len,
76
+ )
77
+ report = inspector.inspect_file(file_path)
78
+ console.print(f"Total pairs: {report.total_pairs} | Valid pairs: {report.valid_pairs_count}")
79
+ if report.invalid_pairs_count > 0 and strict:
80
+ raise typer.Exit(code=1)
trainsight/cli/demo.py ADDED
@@ -0,0 +1,131 @@
1
+ import json
2
+ import tempfile
3
+ from pathlib import Path
4
+ import typer
5
+ from rich.panel import Panel
6
+ import torch
7
+ from transformers import AutoTokenizer
8
+
9
+ from trainsight.inspectors.sft_inspector import SFTInspector
10
+ from trainsight.core.contracts import LossContract
11
+ from trainsight.core.exceptions import ContractViolationError
12
+ from trainsight.reconcilers.sft_reconciler import SFTReconciler
13
+ from trainsight.utils.logging import get_console
14
+
15
+ app = typer.Typer(help="Run interactive demonstration of preflight and batch checks.")
16
+
17
+ DEMO_CLEAN = [
18
+ {"id": "row_001", "prompt": "What is the capital of France?", "completion": "The capital of France is Paris."},
19
+ {"id": "row_002", "prompt": "Solve: 15 * 8", "completion": "15 * 8 = 120."},
20
+ {"id": "row_003", "prompt": "Is 37 a prime number?", "completion": "Yes, 37 is a prime number."},
21
+ ]
22
+
23
+ @app.callback(invoke_without_command=True)
24
+ def run_demo():
25
+ console = get_console()
26
+ console.print("\n[bold cyan]========================================================================[/]")
27
+ console.print("[bold cyan] TrainSight: Preflight LLM Training Input & Loss Contract Auditor [/]")
28
+ console.print("[bold cyan]========================================================================[/]\n")
29
+ console.print("[dim]Note: First run downloads tokenizer configuration files if not already cached.[/]\n")
30
+
31
+ model_name = "Qwen/Qwen2.5-0.5B-Instruct"
32
+ console.print(f"[dim]Loading tokenizer ({model_name})...[/]")
33
+ try:
34
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
35
+ except Exception as e:
36
+ console.print(f"[bold red]Failed to load tokenizer:[/] {e}")
37
+ raise typer.Exit(code=1)
38
+
39
+ with tempfile.TemporaryDirectory() as tmp_dir:
40
+ tmp_path = Path(tmp_dir)
41
+
42
+ # -------------------------------------------------------------
43
+ # STEP 1: Clean Input Verification (Validated against actual report)
44
+ # -------------------------------------------------------------
45
+ console.print("[bold]Step 1: Inspecting clean synthetic dataset (3 rows)...[/]")
46
+ clean_file = tmp_path / "clean.jsonl"
47
+ with open(clean_file, "w", encoding="utf-8") as f:
48
+ for r in DEMO_CLEAN:
49
+ f.write(json.dumps(r) + "\n")
50
+
51
+ inspector = SFTInspector(tokenizer_name=model_name, max_seq_len_threshold=256)
52
+ clean_report = inspector.inspect_file(clean_file)
53
+
54
+ console.print(f" • Total samples: {clean_report.total_samples} | Valid: {clean_report.valid_samples_count}")
55
+ console.print(f" • Empty completions: {clean_report.empty_completion_count} | Truncated answers: {clean_report.truncated_answers_count}")
56
+
57
+ # Check actual report before declaring pass
58
+ clean_passed = (
59
+ clean_report.valid_samples_count == clean_report.total_samples
60
+ and clean_report.empty_completion_count == 0
61
+ and clean_report.truncated_answers_count == 0
62
+ and clean_report.loss_mask_misaligned_count == 0
63
+ )
64
+ if clean_passed:
65
+ console.print("[bold green]✓ Preflight Quality Gate Passed (Zero False Alarms)[/]\n")
66
+ else:
67
+ console.print("[bold red]✗ Unexpected Preflight Failure on Clean Data[/]\n")
68
+
69
+ # -------------------------------------------------------------
70
+ # STEP 2: Truncation Elimination Fault (Validated against actual report)
71
+ # -------------------------------------------------------------
72
+ console.print("[bold]Step 2: Injecting deliberate truncation fault (Answer eliminated by budget)...[/]")
73
+ fault_file = tmp_path / "fault_truncation.jsonl"
74
+ long_prompt = "Verbose prompt designed to consume token budget. " * 30
75
+ with open(fault_file, "w", encoding="utf-8") as f:
76
+ f.write(json.dumps({"id": "row_004", "prompt": long_prompt, "completion": "Answer that will be clipped."}) + "\n")
77
+
78
+ fault_report = inspector.inspect_file(fault_file)
79
+ if fault_report.truncated_answers_count > 0:
80
+ console.print("[bold red]✗ Preflight Quality Gate Caught Fault from Report Results:[/]")
81
+ console.print(Panel(
82
+ f"[bold red]CRITICAL CONTRACT VIOLATION:[/] {fault_report.truncated_answers_count} answer(s) eliminated by truncation.\\n"
83
+ f"Row ID: [bold]row_004[/]\\n"
84
+ f"• Prompt consumed available token budget (max_seq_len=256).\\n"
85
+ f"• Retained answer tokens: 0.\\n"
86
+ f"• Impact: Example retains zero supervised targets.",
87
+ title="TrainSight Preflight Gate (Actual Inspection Report)",
88
+ border_style="red",
89
+ ))
90
+ else:
91
+ console.print("[bold yellow]! Preflight Gate did not flag truncation on fault file.[/]")
92
+
93
+ # -------------------------------------------------------------
94
+ # STEP 3: Synthetic Corrupted Tensor Batch Audit
95
+ # -------------------------------------------------------------
96
+ console.print("\n[bold]Step 3: Auditing a synthetic corrupted tensor batch (prompt unmasking leak)...[/]")
97
+ p_text = tokenizer.apply_chat_template([{"role": "user", "content": "Hello"}], tokenize=False, add_generation_prompt=True)
98
+ c_text = "Hi there!<|im_end|>"
99
+ p_ids = tokenizer.encode(p_text, add_special_tokens=False)
100
+ c_ids = tokenizer.encode(c_text, add_special_tokens=False)
101
+ full_ids = p_ids + c_ids
102
+
103
+ # Construct a synthetic corrupted collator output: prompt unmasked (labels[:p_len] != -100)
104
+ corrupted_batch = {
105
+ "input_ids": torch.tensor([full_ids]),
106
+ "labels": torch.tensor([full_ids]), # Corrupted: prompt labels equal token IDs instead of -100
107
+ "attention_mask": torch.tensor([[1] * len(full_ids)]),
108
+ }
109
+
110
+ reconciler = SFTReconciler(tokenizer=tokenizer, contract=LossContract(strict=False))
111
+ audit = reconciler.audit_batch(batch=corrupted_batch, prompt_lengths=[len(p_ids)], row_ids=["row_synthetic_0"])
112
+
113
+ if not audit.passed and len(audit.diagnoses) > 0:
114
+ diag = audit.diagnoses[0]
115
+ console.print("[bold red]✗ Batch Reconciler Caught Post-Collation Mask Leakage:[/]")
116
+ console.print(Panel(
117
+ f"[bold red]COLLATOR LEAKAGE:[/] {diag.error_code}\\n"
118
+ f"{diag.error_message}\\n"
119
+ f"• Prompt length: {diag.evidence.prompt_token_length} tokens.\\n"
120
+ f"• Unmasked prompt tokens receiving loss: {diag.evidence.unmasked_prompt_tokens_count}.\\n"
121
+ f"• Impact: Gradients would be computed over user prompt tokens.",
122
+ title="TrainSight Batch Reconciler (Synthetic Corrupted Batch)",
123
+ border_style="red",
124
+ ))
125
+ else:
126
+ console.print("[bold green]✓ Reconciler passed synthetic batch.[/]")
127
+
128
+ console.print("\n[bold green]========================================================================[/]")
129
+ console.print("[bold green]✓ Demo Complete: Verified preflight gate and batch reconciler diagnostics.[/]")
130
+ console.print("[bold green]========================================================================[/]\n")
131
+ console.print("To inspect your own dataset:\n $ [bold cyan]trainsight check my_data.jsonl --tokenizer Qwen/Qwen2.5-0.5B-Instruct[/]\n")
trainsight/cli/main.py ADDED
@@ -0,0 +1,22 @@
1
+ import typer
2
+ from trainsight import __version__
3
+ from trainsight.cli.check import check
4
+ from trainsight.cli.demo import run_demo
5
+ from trainsight.utils.logging import get_console
6
+
7
+ app = typer.Typer(
8
+ name="trainsight",
9
+ help="Preflight checks for LLM training inputs and loss contracts.",
10
+ add_completion=False,
11
+ )
12
+
13
+ app.command(name="check")(check)
14
+ app.command(name="demo")(run_demo)
15
+
16
+ @app.command(name="version")
17
+ def version():
18
+ """Prints TrainSight version."""
19
+ get_console().print(f"TrainSight version: [bold cyan]{__version__}[/]")
20
+
21
+ if __name__ == "__main__":
22
+ app()
@@ -0,0 +1,13 @@
1
+ from trainsight.core.contracts import LossContract
2
+ from trainsight.core.exceptions import TrainSightError, ContractViolationError, PreflightQualityGateError
3
+ from trainsight.core.report import AuditReport, RowDiagnosis, TensorEvidence
4
+
5
+ __all__ = [
6
+ "LossContract",
7
+ "TrainSightError",
8
+ "ContractViolationError",
9
+ "PreflightQualityGateError",
10
+ "AuditReport",
11
+ "RowDiagnosis",
12
+ "TensorEvidence",
13
+ ]
@@ -0,0 +1,15 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional, Literal
3
+
4
+ @dataclass
5
+ class LossContract:
6
+ """Declared contract for loss calculation on training batches."""
7
+ task_type: Literal["sft", "dpo"] = "sft"
8
+ chat_template: str = "chatml"
9
+ max_seq_len: int = 2048
10
+ answer_only_loss: bool = True
11
+ mask_padding: bool = True
12
+ require_causal_shift_supervision: bool = True
13
+ strict: bool = True
14
+ expected_template_prefix: str = "<|im_start|>user"
15
+ expected_template_suffix: str = "<|im_start|>assistant\n"
@@ -0,0 +1,14 @@
1
+ class TrainSightError(Exception):
2
+ """Base exception for all TrainSight errors."""
3
+ pass
4
+
5
+ class ContractViolationError(TrainSightError):
6
+ """Raised when actual training tensors violate the intended loss contract."""
7
+ def __init__(self, check_name: str, message: str, row_id: str = "unknown"):
8
+ self.check_name = check_name
9
+ self.row_id = row_id
10
+ super().__init__(f"[{check_name}] (Row: {row_id}): {message}")
11
+
12
+ class PreflightQualityGateError(TrainSightError):
13
+ """Raised when raw dataset inspection fails a pre-flight quality gate."""
14
+ pass
@@ -0,0 +1,48 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Dict, Any, Optional
3
+
4
+ @dataclass
5
+ class TensorEvidence:
6
+ row_id: str
7
+ sequence_length: int
8
+ prompt_token_length: int
9
+ active_labels_count: int
10
+ causal_loss_contributing_tokens: int
11
+ unmasked_prompt_tokens_count: int
12
+ unmasked_padding_tokens_count: int
13
+ decoded_prompt_preview: str
14
+ decoded_answer_preview: str
15
+ labels_sample: List[Any]
16
+
17
+ @dataclass
18
+ class RowDiagnosis:
19
+ row_id: str
20
+ error_code: str
21
+ error_message: str
22
+ severity: str = "critical" # "critical" or "warning"
23
+ evidence: Optional[TensorEvidence] = None
24
+
25
+ @dataclass
26
+ class AuditReport:
27
+ total_inspected: int
28
+ valid_count: int
29
+ violations_count: int
30
+ passed: bool
31
+ diagnoses: List[RowDiagnosis] = field(default_factory=list)
32
+
33
+ def summary_dict(self) -> Dict[str, Any]:
34
+ return {
35
+ "total_inspected": self.total_inspected,
36
+ "valid_count": self.valid_count,
37
+ "violations_count": self.violations_count,
38
+ "passed": self.passed,
39
+ "diagnoses": [
40
+ {
41
+ "row_id": d.row_id,
42
+ "error_code": d.error_code,
43
+ "error_message": d.error_message,
44
+ "severity": d.severity,
45
+ }
46
+ for d in self.diagnoses
47
+ ],
48
+ }
@@ -0,0 +1,5 @@
1
+ from trainsight.inspectors.base import BaseInspector
2
+ from trainsight.inspectors.sft_inspector import SFTInspector, SFTReport
3
+ from trainsight.inspectors.dpo_inspector import DPOInspector, DPOReport
4
+
5
+ __all__ = ["BaseInspector", "SFTInspector", "SFTReport", "DPOInspector", "DPOReport"]
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ class BaseInspector(ABC):
6
+ """Abstract base class for dataset inspectors."""
7
+ @abstractmethod
8
+ def inspect_file(self, file_path: Path) -> Any:
9
+ pass