Training pipeline scripts
Browse files- training/__init__.py +13 -0
- training/data/__init__.py +23 -0
- training/data/preprocess.py +223 -0
- training/data/preprocessing_config.json +26 -0
- training/demo/app.py +322 -0
- training/environment/__init__.py +4 -0
- training/environment/environment.py +131 -0
- training/environment/rewards.py +142 -0
- training/inference/__init__.py +1 -0
- training/inference/benchmark_latency.py +147 -0
- training/inference/strategy_engine.py +307 -0
- training/requirements.txt +12 -0
- training/run_pipeline.py +173 -0
training/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
KernelX Intelligence Layer — Model Training Pipeline
|
| 3 |
+
|
| 4 |
+
Stages:
|
| 5 |
+
0. Environment setup (requirements.txt)
|
| 6 |
+
1. Data preprocessing (data/preprocess.py)
|
| 7 |
+
2. World Model SFT training (models/train_world_model.py)
|
| 8 |
+
3. Strategist RL training via GRPO (models/train_strategist.py)
|
| 9 |
+
4. Monitoring (logged during training)
|
| 10 |
+
5. Export & quantization to GGUF (models/export_gguf.py)
|
| 11 |
+
6. Inference engine (inference/strategy_engine.py)
|
| 12 |
+
7. Gradio demo (demo/app.py)
|
| 13 |
+
"""
|
training/data/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""KernelX training data preprocessing utilities."""
|
| 2 |
+
|
| 3 |
+
from .preprocess import (
|
| 4 |
+
CONFIG,
|
| 5 |
+
ACTIVE_FEATURES,
|
| 6 |
+
FEATURE_NAMES,
|
| 7 |
+
SYMLOG_FEATURES,
|
| 8 |
+
IDX_CPU,
|
| 9 |
+
IDX_PRIO,
|
| 10 |
+
IDX_STATIC_PRIO,
|
| 11 |
+
IDX_NORMAL_PRIO,
|
| 12 |
+
IDX_EXEC_NS,
|
| 13 |
+
IDX_VRUNTIME,
|
| 14 |
+
IDX_MIGRATIONS,
|
| 15 |
+
IDX_CPUS_ALLOWED,
|
| 16 |
+
IDX_CTX_SWITCHES,
|
| 17 |
+
IDX_WAIT_US,
|
| 18 |
+
symmetric_log,
|
| 19 |
+
preprocess_features,
|
| 20 |
+
extract_active,
|
| 21 |
+
format_state,
|
| 22 |
+
preprocess_record,
|
| 23 |
+
)
|
training/data/preprocess.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
KernelX Intelligence Layer — Data Ingestion and Preprocessing (Stage 1)
|
| 3 |
+
|
| 4 |
+
Reads raw state_transitions.jsonl from the bridge's TrajectoryManager,
|
| 5 |
+
applies feature scaling (symlog for huge counters), drops sparse-zero
|
| 6 |
+
features, and produces train/val/test splits for World Model and
|
| 7 |
+
Strategist training.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python -m training.data.preprocess --input data/state_transitions.jsonl
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
import argparse
|
| 15 |
+
import sys
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from typing import List, Dict, Any
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
# ---------------------------------------------------------------------------
|
| 22 |
+
# Configuration (loaded from preprocessing_config.json)
|
| 23 |
+
# ---------------------------------------------------------------------------
|
| 24 |
+
|
| 25 |
+
CONFIG_PATH = Path(__file__).parent / "preprocessing_config.json"
|
| 26 |
+
|
| 27 |
+
def load_config() -> dict:
|
| 28 |
+
with open(CONFIG_PATH) as f:
|
| 29 |
+
return json.load(f)
|
| 30 |
+
|
| 31 |
+
CONFIG = load_config()
|
| 32 |
+
|
| 33 |
+
SYMLOG_FEATURES = CONFIG["symlog_features"] # [4, 5, 6]
|
| 34 |
+
ACTIVE_FEATURES = CONFIG["active_features"] # [0,1,2,3,4,5,6,7,12,23]
|
| 35 |
+
FEATURE_NAMES = CONFIG["feature_names"] # 10 short names
|
| 36 |
+
SPARSE_ZERO = CONFIG["sparse_zero_features"] # indices to drop
|
| 37 |
+
|
| 38 |
+
# ---------------------------------------------------------------------------
|
| 39 |
+
# Feature index constants (positions within the ACTIVE 10D vector)
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
|
| 42 |
+
IDX_CPU = 0 # raw index 0
|
| 43 |
+
IDX_PRIO = 1 # raw index 1
|
| 44 |
+
IDX_STATIC_PRIO = 2 # raw index 2
|
| 45 |
+
IDX_NORMAL_PRIO = 3 # raw index 3
|
| 46 |
+
IDX_EXEC_NS = 4 # raw index 4 (symlog)
|
| 47 |
+
IDX_VRUNTIME = 5 # raw index 5 (symlog)
|
| 48 |
+
IDX_MIGRATIONS = 6 # raw index 6 (symlog)
|
| 49 |
+
IDX_CPUS_ALLOWED = 7 # raw index 7
|
| 50 |
+
IDX_CTX_SWITCHES = 8 # raw index 12
|
| 51 |
+
IDX_WAIT_US = 9 # raw index 23
|
| 52 |
+
|
| 53 |
+
# ---------------------------------------------------------------------------
|
| 54 |
+
# Scaling functions
|
| 55 |
+
# ---------------------------------------------------------------------------
|
| 56 |
+
|
| 57 |
+
def symmetric_log(x: float) -> float:
|
| 58 |
+
"""sgn(x) * ln(1 + |x|) — compresses huge values while preserving sign."""
|
| 59 |
+
return float(np.sign(x) * np.log1p(np.abs(x)))
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def preprocess_features(raw_features: List[float]) -> List[float]:
|
| 63 |
+
"""Transform a raw 24D feature vector: symlog the big counters."""
|
| 64 |
+
f = list(raw_features) # copy
|
| 65 |
+
for idx in SYMLOG_FEATURES:
|
| 66 |
+
f[idx] = symmetric_log(f[idx])
|
| 67 |
+
return f
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def extract_active(scaled_features: List[float]) -> List[float]:
|
| 71 |
+
"""Keep only the active (non-zero, information-carrying) features."""
|
| 72 |
+
return [scaled_features[i] for i in ACTIVE_FEATURES]
|
| 73 |
+
|
| 74 |
+
# ---------------------------------------------------------------------------
|
| 75 |
+
# Prompt formatting for LLM consumption
|
| 76 |
+
# ---------------------------------------------------------------------------
|
| 77 |
+
|
| 78 |
+
def format_state(active_vector: List[float]) -> str:
|
| 79 |
+
"""Convert a 10D active feature vector into a compact text string."""
|
| 80 |
+
parts = []
|
| 81 |
+
for name, val in zip(FEATURE_NAMES, active_vector):
|
| 82 |
+
if val == int(val):
|
| 83 |
+
parts.append(f"{name}:{int(val)}")
|
| 84 |
+
else:
|
| 85 |
+
parts.append(f"{name}:{val:.2f}")
|
| 86 |
+
return " | ".join(parts)
|
| 87 |
+
|
| 88 |
+
# ---------------------------------------------------------------------------
|
| 89 |
+
# Record processing
|
| 90 |
+
# ---------------------------------------------------------------------------
|
| 91 |
+
|
| 92 |
+
def preprocess_record(record: Dict[str, Any]) -> Dict[str, Any]:
|
| 93 |
+
"""Transform a single raw JSONL record into training-ready format."""
|
| 94 |
+
s_t_scaled = preprocess_features(record["state_t"]["features"])
|
| 95 |
+
s_t1_scaled = preprocess_features(record["state_t_next"]["features"])
|
| 96 |
+
|
| 97 |
+
s_t_active = extract_active(s_t_scaled)
|
| 98 |
+
s_t1_active = extract_active(s_t1_scaled)
|
| 99 |
+
|
| 100 |
+
return {
|
| 101 |
+
"state": s_t_active,
|
| 102 |
+
"action": record["action"],
|
| 103 |
+
"reward": record["reward"],
|
| 104 |
+
"next_state": s_t1_active,
|
| 105 |
+
"pid": record["state_t"]["pid"],
|
| 106 |
+
"cpu": record["state_t"]["cpu"],
|
| 107 |
+
"timestamp": record["state_t"]["timestamp"],
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
# ---------------------------------------------------------------------------
|
| 111 |
+
# Dataset audit
|
| 112 |
+
# ---------------------------------------------------------------------------
|
| 113 |
+
|
| 114 |
+
def audit_dataset(records: List[Dict]) -> None:
|
| 115 |
+
"""Print per-feature statistics for the raw dataset."""
|
| 116 |
+
all_features = []
|
| 117 |
+
for r in records:
|
| 118 |
+
all_features.append(r["state_t"]["features"])
|
| 119 |
+
all_features.append(r["state_t_next"]["features"])
|
| 120 |
+
|
| 121 |
+
arr = np.array(all_features, dtype=np.float64)
|
| 122 |
+
|
| 123 |
+
print(f"\nTotal transitions: {len(records)}")
|
| 124 |
+
print(f"Total feature vectors: {len(all_features)}")
|
| 125 |
+
print(f"\n{'Idx':<5} {'Min':<22} {'Max':<22} {'Mean':<22} {'Std':<22} {'Zeros%':<10}")
|
| 126 |
+
print("-" * 103)
|
| 127 |
+
for i in range(24):
|
| 128 |
+
col = arr[:, i]
|
| 129 |
+
zero_pct = (col == 0).sum() / len(col) * 100
|
| 130 |
+
print(f"{i:<5} {col.min():<22.2f} {col.max():<22.2f} {col.mean():<22.2f} {col.std():<22.2f} {zero_pct:<10.1f}")
|
| 131 |
+
|
| 132 |
+
print(f"\nNaN count: {np.isnan(arr).sum()}")
|
| 133 |
+
print(f"Inf count: {np.isinf(arr).sum()}")
|
| 134 |
+
|
| 135 |
+
actions = [r["action"] for r in records]
|
| 136 |
+
rewards = [r["reward"] for r in records]
|
| 137 |
+
print(f"\nAction — unique values: {sorted(set(actions))}")
|
| 138 |
+
print(f"Reward — min: {min(rewards)}, max: {max(rewards)}, mean: {np.mean(rewards):.2f}, std: {np.std(rewards):.2f}")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# ---------------------------------------------------------------------------
|
| 142 |
+
# Train / Val / Test split (chronological)
|
| 143 |
+
# ---------------------------------------------------------------------------
|
| 144 |
+
|
| 145 |
+
def split_chronological(processed: List[Dict], train_ratio=0.8, val_ratio=0.1):
|
| 146 |
+
"""Split processed records chronologically (NOT randomly)."""
|
| 147 |
+
processed.sort(key=lambda x: x["timestamp"])
|
| 148 |
+
n = len(processed)
|
| 149 |
+
train_end = int(n * train_ratio)
|
| 150 |
+
val_end = int(n * (train_ratio + val_ratio))
|
| 151 |
+
return processed[:train_end], processed[train_end:val_end], processed[val_end:]
|
| 152 |
+
|
| 153 |
+
# ---------------------------------------------------------------------------
|
| 154 |
+
# Main pipeline
|
| 155 |
+
# ---------------------------------------------------------------------------
|
| 156 |
+
|
| 157 |
+
def run_pipeline(input_path: str, output_dir: str, audit: bool = True):
|
| 158 |
+
"""Full preprocessing pipeline: audit -> scale -> split -> save."""
|
| 159 |
+
output_dir = Path(output_dir)
|
| 160 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 161 |
+
|
| 162 |
+
# Load raw data
|
| 163 |
+
print(f"Loading raw data from {input_path} ...")
|
| 164 |
+
records = []
|
| 165 |
+
with open(input_path) as f:
|
| 166 |
+
for line in f:
|
| 167 |
+
line = line.strip()
|
| 168 |
+
if line:
|
| 169 |
+
records.append(json.loads(line))
|
| 170 |
+
|
| 171 |
+
if not records:
|
| 172 |
+
print("ERROR: No records found in input file.")
|
| 173 |
+
sys.exit(1)
|
| 174 |
+
|
| 175 |
+
print(f"Loaded {len(records)} raw transitions.")
|
| 176 |
+
|
| 177 |
+
# Audit
|
| 178 |
+
if audit:
|
| 179 |
+
audit_dataset(records)
|
| 180 |
+
|
| 181 |
+
# Preprocess
|
| 182 |
+
print("\nPreprocessing (symlog scaling + active feature extraction) ...")
|
| 183 |
+
processed = [preprocess_record(r) for r in records]
|
| 184 |
+
|
| 185 |
+
# Verify transform on first record
|
| 186 |
+
sample = processed[0]
|
| 187 |
+
print(f"\nSample preprocessed state (10D):")
|
| 188 |
+
print(f" {format_state(sample['state'])}")
|
| 189 |
+
print(f"Sample preprocessed next_state:")
|
| 190 |
+
print(f" {format_state(sample['next_state'])}")
|
| 191 |
+
|
| 192 |
+
# Save full processed dataset
|
| 193 |
+
processed_path = output_dir / "processed_transitions.jsonl"
|
| 194 |
+
with open(processed_path, "w") as f:
|
| 195 |
+
for p in processed:
|
| 196 |
+
f.write(json.dumps(p) + "\n")
|
| 197 |
+
print(f"\nSaved {len(processed)} processed records to {processed_path}")
|
| 198 |
+
|
| 199 |
+
# Split
|
| 200 |
+
train, val, test = split_chronological(processed)
|
| 201 |
+
for split_name, split_data in [("train", train), ("val", val), ("test", test)]:
|
| 202 |
+
split_path = output_dir / f"{split_name}.jsonl"
|
| 203 |
+
with open(split_path, "w") as f:
|
| 204 |
+
for item in split_data:
|
| 205 |
+
f.write(json.dumps(item) + "\n")
|
| 206 |
+
print(f"{split_name}: {len(split_data)} records -> {split_path}")
|
| 207 |
+
|
| 208 |
+
print("\nPreprocessing complete.")
|
| 209 |
+
return train, val, test
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def main():
|
| 213 |
+
parser = argparse.ArgumentParser(description="KernelX data preprocessing pipeline")
|
| 214 |
+
parser.add_argument("--input", required=True, help="Path to raw state_transitions.jsonl")
|
| 215 |
+
parser.add_argument("--output-dir", default=str(Path(__file__).parent), help="Output directory for processed data")
|
| 216 |
+
parser.add_argument("--no-audit", action="store_true", help="Skip the dataset audit step")
|
| 217 |
+
args = parser.parse_args()
|
| 218 |
+
|
| 219 |
+
run_pipeline(args.input, args.output_dir, audit=not args.no_audit)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
if __name__ == "__main__":
|
| 223 |
+
main()
|
training/data/preprocessing_config.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"symlog_features": [4, 5, 6],
|
| 3 |
+
"active_features": [0, 1, 2, 3, 4, 5, 6, 7, 12, 23],
|
| 4 |
+
"feature_names": ["cpu", "prio", "sprio", "nprio", "exec_ns", "vrt", "migr", "cpus", "csw", "wt_us"],
|
| 5 |
+
"passthrough_features": [0, 7, 12, 23],
|
| 6 |
+
"priority_features": [1, 2, 3],
|
| 7 |
+
"sparse_zero_features": [8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22],
|
| 8 |
+
"feature_descriptions": {
|
| 9 |
+
"0": "bpf_get_smp_processor_id() - CPU core ID",
|
| 10 |
+
"1": "task->prio - Dynamic priority (0-139)",
|
| 11 |
+
"2": "task->static_prio - Static priority (nice-based)",
|
| 12 |
+
"3": "task->normal_prio - Normal priority",
|
| 13 |
+
"4": "task->se.sum_exec_runtime - Total CPU time in ns (symlog-scaled)",
|
| 14 |
+
"5": "task->se.vruntime - CFS virtual runtime (symlog-scaled)",
|
| 15 |
+
"6": "task->se.nr_migrations - CPU migration count (symlog-scaled)",
|
| 16 |
+
"7": "task->nr_cpus_allowed - CPU affinity mask size",
|
| 17 |
+
"12": "context switch count from cpu_stats",
|
| 18 |
+
"23": "wait time in microseconds ((now - start_ts) / 1000)"
|
| 19 |
+
},
|
| 20 |
+
"model": {
|
| 21 |
+
"name": "HuggingFaceTB/SmolLM2-360M-Instruct",
|
| 22 |
+
"backup": "Qwen/Qwen2.5-0.5B-Instruct",
|
| 23 |
+
"max_seq_length": 512,
|
| 24 |
+
"target_inference_ms": 50
|
| 25 |
+
}
|
| 26 |
+
}
|
training/demo/app.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
KernelX Intelligence Layer — Gradio Demo (Stage 7)
|
| 3 |
+
|
| 4 |
+
Judge-facing demo that shows:
|
| 5 |
+
1. Baseline SmolLM2-360M output (untrained) on kernel states
|
| 6 |
+
2. World Model predictions vs actual next states
|
| 7 |
+
3. Strategist scheduling actions (action-only, no rationale for speed)
|
| 8 |
+
4. Before/after metrics comparison
|
| 9 |
+
|
| 10 |
+
Usage:
|
| 11 |
+
python -m training.demo.app \
|
| 12 |
+
--strategist-model training/models/strategist_merged/strategist-q4km.gguf \
|
| 13 |
+
--test-data training/data/test.jsonl
|
| 14 |
+
|
| 15 |
+
# Without trained models (shows heuristic baseline):
|
| 16 |
+
python -m training.demo.app --test-data training/data/test.jsonl --no-model
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import argparse
|
| 20 |
+
import json
|
| 21 |
+
import re
|
| 22 |
+
import sys
|
| 23 |
+
import time
|
| 24 |
+
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from typing import Optional
|
| 27 |
+
|
| 28 |
+
import numpy as np
|
| 29 |
+
|
| 30 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
| 31 |
+
from training.data.preprocess import (
|
| 32 |
+
FEATURE_NAMES, format_state, load_config,
|
| 33 |
+
IDX_WAIT_US, IDX_CTX_SWITCHES, IDX_EXEC_NS,
|
| 34 |
+
)
|
| 35 |
+
from training.environment.rewards import RewardComputer
|
| 36 |
+
|
| 37 |
+
CONFIG = load_config()
|
| 38 |
+
|
| 39 |
+
# ---------------------------------------------------------------------------
|
| 40 |
+
# Heuristic baseline policy (for comparison)
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
|
| 43 |
+
def heuristic_policy(state: list) -> float:
|
| 44 |
+
"""Simple rule-based policy for baseline comparison."""
|
| 45 |
+
wait_us = state[IDX_WAIT_US]
|
| 46 |
+
csw = state[IDX_CTX_SWITCHES]
|
| 47 |
+
|
| 48 |
+
if wait_us > 15:
|
| 49 |
+
return -0.6
|
| 50 |
+
elif csw > 10:
|
| 51 |
+
return -0.3
|
| 52 |
+
else:
|
| 53 |
+
return 0.05
|
| 54 |
+
|
| 55 |
+
# ---------------------------------------------------------------------------
|
| 56 |
+
# Model wrappers
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
|
| 59 |
+
class StrategistWrapper:
|
| 60 |
+
"""Wraps a GGUF model for use in the demo."""
|
| 61 |
+
|
| 62 |
+
def __init__(self, model_path: str):
|
| 63 |
+
from llama_cpp import Llama
|
| 64 |
+
self.llm = Llama(model_path=model_path, n_ctx=512, n_threads=4, verbose=False)
|
| 65 |
+
|
| 66 |
+
def predict(self, state: list, pid: int, cpu: int) -> tuple:
|
| 67 |
+
state_str = format_state(state)
|
| 68 |
+
prompt = (
|
| 69 |
+
"<|system|>You are a Linux kernel scheduling strategist. "
|
| 70 |
+
"Given the current system state, output a scheduling action.<|end|>\n"
|
| 71 |
+
f"<|user|>[STATE] {state_str}\n"
|
| 72 |
+
f"[PID] {pid} [CPU] {cpu}\n"
|
| 73 |
+
"[ACTION]<|end|>\n"
|
| 74 |
+
"<|assistant|>"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
start = time.perf_counter()
|
| 78 |
+
output = self.llm(prompt, max_tokens=8, temperature=0.2)
|
| 79 |
+
latency = (time.perf_counter() - start) * 1000
|
| 80 |
+
|
| 81 |
+
text = output["choices"][0]["text"]
|
| 82 |
+
action_match = re.search(r"([-+]?\d*\.?\d+)", text)
|
| 83 |
+
|
| 84 |
+
action = float(action_match.group(1)) if action_match else 0.0
|
| 85 |
+
action = max(-1.0, min(1.0, action))
|
| 86 |
+
|
| 87 |
+
return action, latency
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class WorldModelWrapper:
|
| 91 |
+
"""Wraps a GGUF or HF model for world model predictions."""
|
| 92 |
+
|
| 93 |
+
def __init__(self, model_path: str):
|
| 94 |
+
from llama_cpp import Llama
|
| 95 |
+
self.llm = Llama(model_path=model_path, n_ctx=512, n_threads=4, verbose=False)
|
| 96 |
+
|
| 97 |
+
def predict_next_state(self, state: list, action: float, pid: int) -> list:
|
| 98 |
+
state_str = format_state(state)
|
| 99 |
+
prompt = (
|
| 100 |
+
"<|system|>You are a Linux kernel simulator. "
|
| 101 |
+
"Predict the next system state.<|end|>\n"
|
| 102 |
+
f"<|user|>[STATE] {state_str}\n"
|
| 103 |
+
f"[ACTION] {action:.4f}\n"
|
| 104 |
+
f"[PID] {pid}\n"
|
| 105 |
+
"Predict [NEXT_STATE]<|end|>\n"
|
| 106 |
+
"<|assistant|>"
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
output = self.llm(prompt, max_tokens=128, temperature=0.1)
|
| 110 |
+
text = output["choices"][0]["text"]
|
| 111 |
+
|
| 112 |
+
# Parse predicted state
|
| 113 |
+
values = []
|
| 114 |
+
for part in text.split("|"):
|
| 115 |
+
part = part.strip()
|
| 116 |
+
if ":" in part:
|
| 117 |
+
try:
|
| 118 |
+
values.append(float(part.split(":")[1]))
|
| 119 |
+
except ValueError:
|
| 120 |
+
pass
|
| 121 |
+
|
| 122 |
+
if len(values) == len(FEATURE_NAMES):
|
| 123 |
+
return values
|
| 124 |
+
return state # fallback: return same state
|
| 125 |
+
|
| 126 |
+
# ---------------------------------------------------------------------------
|
| 127 |
+
# Demo functions
|
| 128 |
+
# ---------------------------------------------------------------------------
|
| 129 |
+
|
| 130 |
+
def run_comparison(
|
| 131 |
+
record: dict,
|
| 132 |
+
strategist: Optional[StrategistWrapper],
|
| 133 |
+
reward_computer: RewardComputer,
|
| 134 |
+
) -> dict:
|
| 135 |
+
"""Run baseline vs trained comparison on a single transition."""
|
| 136 |
+
state = record["state"]
|
| 137 |
+
next_state = record["next_state"]
|
| 138 |
+
pid = record["pid"]
|
| 139 |
+
cpu = record["cpu"]
|
| 140 |
+
|
| 141 |
+
# Heuristic baseline
|
| 142 |
+
h_action = heuristic_policy(state)
|
| 143 |
+
h_reward = reward_computer.compute_total(
|
| 144 |
+
state=state, action=h_action, prev_action=0.0,
|
| 145 |
+
next_state=next_state,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
result = {
|
| 149 |
+
"state": format_state(state),
|
| 150 |
+
"actual_next_state": format_state(next_state),
|
| 151 |
+
"pid": pid,
|
| 152 |
+
"cpu": cpu,
|
| 153 |
+
"heuristic": {
|
| 154 |
+
"action": h_action,
|
| 155 |
+
"reward": h_reward,
|
| 156 |
+
},
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
# Trained strategist
|
| 160 |
+
if strategist:
|
| 161 |
+
s_action, s_latency = strategist.predict(state, pid, cpu)
|
| 162 |
+
s_reward = reward_computer.compute_total(
|
| 163 |
+
state=state, action=s_action, prev_action=0.0,
|
| 164 |
+
next_state=next_state,
|
| 165 |
+
)
|
| 166 |
+
result["strategist"] = {
|
| 167 |
+
"action": s_action,
|
| 168 |
+
"latency_ms": s_latency,
|
| 169 |
+
"reward": s_reward,
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
return result
|
| 173 |
+
|
| 174 |
+
# ---------------------------------------------------------------------------
|
| 175 |
+
# Gradio app
|
| 176 |
+
# ---------------------------------------------------------------------------
|
| 177 |
+
|
| 178 |
+
def build_gradio_app(
|
| 179 |
+
test_records: list,
|
| 180 |
+
strategist: Optional[StrategistWrapper],
|
| 181 |
+
world_model: Optional[WorldModelWrapper],
|
| 182 |
+
):
|
| 183 |
+
"""Build and return the Gradio interface."""
|
| 184 |
+
import gradio as gr
|
| 185 |
+
|
| 186 |
+
reward_computer = RewardComputer(alpha=1.0, beta=2.0, gamma=0.5)
|
| 187 |
+
|
| 188 |
+
def analyze_state(record_idx: int):
|
| 189 |
+
"""Run analysis on a selected test record."""
|
| 190 |
+
idx = int(record_idx) % len(test_records)
|
| 191 |
+
record = test_records[idx]
|
| 192 |
+
result = run_comparison(record, strategist, reward_computer)
|
| 193 |
+
|
| 194 |
+
# Format output
|
| 195 |
+
lines = []
|
| 196 |
+
lines.append(f"## State #{idx}")
|
| 197 |
+
lines.append(f"**PID:** {result['pid']} | **CPU:** {result['cpu']}")
|
| 198 |
+
lines.append(f"**Current State:** `{result['state']}`")
|
| 199 |
+
lines.append(f"**Actual Next State:** `{result['actual_next_state']}`")
|
| 200 |
+
lines.append("")
|
| 201 |
+
|
| 202 |
+
# Heuristic
|
| 203 |
+
h = result["heuristic"]
|
| 204 |
+
lines.append("### Heuristic Baseline")
|
| 205 |
+
lines.append(f"- **Action:** {h['action']:.4f}")
|
| 206 |
+
lines.append(f"- **Total Reward:** {h['reward']['total']:.4f}")
|
| 207 |
+
lines.append(f" - Throughput: {h['reward']['throughput']:.4f}")
|
| 208 |
+
lines.append(f" - Latency: {h['reward']['latency']:.4f}")
|
| 209 |
+
lines.append(f" - Stability: {h['reward']['stability']:.4f}")
|
| 210 |
+
lines.append("")
|
| 211 |
+
|
| 212 |
+
# Strategist
|
| 213 |
+
if "strategist" in result:
|
| 214 |
+
s = result["strategist"]
|
| 215 |
+
lines.append("### Trained Strategist")
|
| 216 |
+
lines.append(f"- **Action:** {s['action']:.4f}")
|
| 217 |
+
lines.append(f"- **Inference Latency:** {s['latency_ms']:.1f}ms")
|
| 218 |
+
lines.append(f"- **Total Reward:** {s['reward']['total']:.4f}")
|
| 219 |
+
lines.append(f" - Throughput: {s['reward']['throughput']:.4f}")
|
| 220 |
+
lines.append(f" - Latency: {s['reward']['latency']:.4f}")
|
| 221 |
+
lines.append(f" - Stability: {s['reward']['stability']:.4f}")
|
| 222 |
+
lines.append("")
|
| 223 |
+
|
| 224 |
+
# Comparison
|
| 225 |
+
delta = s["reward"]["total"] - h["reward"]["total"]
|
| 226 |
+
direction = "better" if delta > 0 else "worse"
|
| 227 |
+
lines.append(f"### Comparison")
|
| 228 |
+
lines.append(f"Strategist is **{delta:+.4f}** reward ({direction} than heuristic)")
|
| 229 |
+
else:
|
| 230 |
+
lines.append("*No trained model loaded. Run with --strategist-model to compare.*")
|
| 231 |
+
|
| 232 |
+
return "\n".join(lines)
|
| 233 |
+
|
| 234 |
+
def batch_comparison():
|
| 235 |
+
"""Run comparison across all test records and report aggregate stats."""
|
| 236 |
+
h_rewards = []
|
| 237 |
+
s_rewards = []
|
| 238 |
+
s_latencies = []
|
| 239 |
+
|
| 240 |
+
n = min(50, len(test_records)) # cap for speed
|
| 241 |
+
for i in range(n):
|
| 242 |
+
result = run_comparison(test_records[i], strategist, reward_computer)
|
| 243 |
+
h_rewards.append(result["heuristic"]["reward"]["total"])
|
| 244 |
+
if "strategist" in result:
|
| 245 |
+
s_rewards.append(result["strategist"]["reward"]["total"])
|
| 246 |
+
s_latencies.append(result["strategist"]["latency_ms"])
|
| 247 |
+
|
| 248 |
+
s_mean = f"{np.mean(s_rewards):.4f}" if s_rewards else "N/A"
|
| 249 |
+
s_std = f"{np.std(s_rewards):.4f}" if s_rewards else "N/A"
|
| 250 |
+
|
| 251 |
+
lines = [f"## Batch Comparison ({n} samples)\n"]
|
| 252 |
+
lines.append("| Metric | Heuristic | Strategist |")
|
| 253 |
+
lines.append("|--------|-----------|------------|")
|
| 254 |
+
lines.append(f"| Mean Reward | {np.mean(h_rewards):.4f} | {s_mean} |")
|
| 255 |
+
lines.append(f"| Std Reward | {np.std(h_rewards):.4f} | {s_std} |")
|
| 256 |
+
if s_latencies:
|
| 257 |
+
lines.append(f"| Mean Latency | N/A | {np.mean(s_latencies):.1f}ms |")
|
| 258 |
+
lines.append(f"| P95 Latency | N/A | {np.percentile(s_latencies, 95):.1f}ms |")
|
| 259 |
+
if s_rewards:
|
| 260 |
+
win_rate = sum(1 for s, h in zip(s_rewards, h_rewards) if s > h) / len(s_rewards)
|
| 261 |
+
lines.append(f"| Win Rate | — | {win_rate*100:.1f}% |")
|
| 262 |
+
|
| 263 |
+
return "\n".join(lines)
|
| 264 |
+
|
| 265 |
+
# Build Gradio UI
|
| 266 |
+
with gr.Blocks(title="KernelX Intelligence Layer") as app:
|
| 267 |
+
gr.Markdown("# KernelX Intelligence Layer Demo")
|
| 268 |
+
gr.Markdown("Compare heuristic baseline vs trained Strategist on real kernel states.")
|
| 269 |
+
|
| 270 |
+
with gr.Row():
|
| 271 |
+
record_slider = gr.Slider(
|
| 272 |
+
minimum=0, maximum=len(test_records) - 1,
|
| 273 |
+
step=1, value=0, label="Test Record Index"
|
| 274 |
+
)
|
| 275 |
+
analyze_btn = gr.Button("Analyze", variant="primary")
|
| 276 |
+
|
| 277 |
+
output_md = gr.Markdown()
|
| 278 |
+
analyze_btn.click(fn=analyze_state, inputs=[record_slider], outputs=[output_md])
|
| 279 |
+
|
| 280 |
+
gr.Markdown("---")
|
| 281 |
+
batch_btn = gr.Button("Run Batch Comparison (50 samples)")
|
| 282 |
+
batch_output = gr.Markdown()
|
| 283 |
+
batch_btn.click(fn=batch_comparison, outputs=[batch_output])
|
| 284 |
+
|
| 285 |
+
return app
|
| 286 |
+
|
| 287 |
+
# ---------------------------------------------------------------------------
|
| 288 |
+
# CLI
|
| 289 |
+
# ---------------------------------------------------------------------------
|
| 290 |
+
|
| 291 |
+
def main():
|
| 292 |
+
parser = argparse.ArgumentParser(description="KernelX Gradio Demo")
|
| 293 |
+
parser.add_argument("--test-data", required=True, help="Path to test.jsonl")
|
| 294 |
+
parser.add_argument("--strategist-model", default=None, help="GGUF Strategist model")
|
| 295 |
+
parser.add_argument("--world-model", default=None, help="GGUF World Model")
|
| 296 |
+
parser.add_argument("--no-model", action="store_true", help="Run without trained models")
|
| 297 |
+
parser.add_argument("--port", type=int, default=7860)
|
| 298 |
+
parser.add_argument("--share", action="store_true", help="Create public Gradio link")
|
| 299 |
+
args = parser.parse_args()
|
| 300 |
+
|
| 301 |
+
# Load test data
|
| 302 |
+
records = [json.loads(l) for l in open(args.test_data) if l.strip()]
|
| 303 |
+
print(f"Loaded {len(records)} test records")
|
| 304 |
+
|
| 305 |
+
# Load models
|
| 306 |
+
strategist = None
|
| 307 |
+
world_model = None
|
| 308 |
+
|
| 309 |
+
if not args.no_model:
|
| 310 |
+
if args.strategist_model:
|
| 311 |
+
print(f"Loading Strategist: {args.strategist_model}")
|
| 312 |
+
strategist = StrategistWrapper(args.strategist_model)
|
| 313 |
+
if args.world_model:
|
| 314 |
+
print(f"Loading World Model: {args.world_model}")
|
| 315 |
+
world_model = WorldModelWrapper(args.world_model)
|
| 316 |
+
|
| 317 |
+
app = build_gradio_app(records, strategist, world_model)
|
| 318 |
+
app.launch(server_port=args.port, share=args.share)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
if __name__ == "__main__":
|
| 322 |
+
main()
|
training/environment/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""KernelX RL environment and reward functions."""
|
| 2 |
+
|
| 3 |
+
from .rewards import RewardComputer
|
| 4 |
+
from .environment import KernelSchedulerEnv, KernelState, KernelAction
|
training/environment/environment.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
KernelX Intelligence Layer — RL Environment (OpenEnv structure)
|
| 3 |
+
|
| 4 |
+
Provides reset/step interface for training the Strategist policy via GRPO.
|
| 5 |
+
Replays recorded transitions from the preprocessed JSONL dataset and
|
| 6 |
+
computes multi-objective rewards.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import random
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from typing import List, Tuple
|
| 13 |
+
|
| 14 |
+
from .rewards import RewardComputer
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class KernelState:
|
| 19 |
+
"""Observation wrapper for the RL environment."""
|
| 20 |
+
features: List[float] # active features (10D after preprocessing)
|
| 21 |
+
pid: int
|
| 22 |
+
cpu: int
|
| 23 |
+
timestep: int
|
| 24 |
+
prev_action: float
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class KernelAction:
|
| 29 |
+
"""Action output from the Strategist."""
|
| 30 |
+
value: float # scheduling weight in [-1.0, 1.0]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class KernelSchedulerEnv:
|
| 34 |
+
"""Offline RL environment that replays recorded kernel transitions.
|
| 35 |
+
|
| 36 |
+
Each episode starts at a random position in the dataset and runs for
|
| 37 |
+
max_steps transitions. The reward is computed from the multi-objective
|
| 38 |
+
RewardComputer.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def __init__(
|
| 42 |
+
self,
|
| 43 |
+
data_path: str = "training/data/train.jsonl",
|
| 44 |
+
max_steps: int = 10,
|
| 45 |
+
alpha: float = 1.0,
|
| 46 |
+
beta: float = 2.0,
|
| 47 |
+
gamma: float = 0.5,
|
| 48 |
+
):
|
| 49 |
+
self.records = [json.loads(l) for l in open(data_path) if l.strip()]
|
| 50 |
+
self.max_steps = max_steps
|
| 51 |
+
self.reward_computer = RewardComputer(alpha=alpha, beta=beta, gamma=gamma)
|
| 52 |
+
|
| 53 |
+
# Episode state
|
| 54 |
+
self.timestep = 0
|
| 55 |
+
self.current_idx = 0
|
| 56 |
+
self.prev_action = 0.0
|
| 57 |
+
|
| 58 |
+
if len(self.records) < max_steps + 1:
|
| 59 |
+
raise ValueError(
|
| 60 |
+
f"Dataset has {len(self.records)} records but max_steps={max_steps} "
|
| 61 |
+
f"requires at least {max_steps + 1}"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
def reset(self) -> KernelState:
|
| 65 |
+
"""Start a fresh episode from a random point in the dataset."""
|
| 66 |
+
self.timestep = 0
|
| 67 |
+
self.current_idx = random.randint(0, len(self.records) - self.max_steps - 1)
|
| 68 |
+
self.prev_action = 0.0
|
| 69 |
+
return self._get_state()
|
| 70 |
+
|
| 71 |
+
def step(self, action: KernelAction) -> Tuple[KernelState, dict, bool]:
|
| 72 |
+
"""Apply action, compute reward, advance to next state.
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
next_state: The new KernelState after the transition
|
| 76 |
+
reward_breakdown: Dict with 'total' and per-component rewards
|
| 77 |
+
done: Whether the episode has ended
|
| 78 |
+
"""
|
| 79 |
+
current = self.records[self.current_idx + self.timestep]
|
| 80 |
+
next_idx = self.current_idx + self.timestep + 1
|
| 81 |
+
next_rec = self.records[next_idx] if next_idx < len(self.records) else current
|
| 82 |
+
|
| 83 |
+
reward_breakdown = self.reward_computer.compute_total(
|
| 84 |
+
state=current["state"],
|
| 85 |
+
action=action,
|
| 86 |
+
prev_action=self.prev_action,
|
| 87 |
+
next_state=next_rec["state"],
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
self.timestep += 1
|
| 91 |
+
self.prev_action = action.value
|
| 92 |
+
done = self.timestep >= self.max_steps
|
| 93 |
+
|
| 94 |
+
return self._get_state(), reward_breakdown, done
|
| 95 |
+
|
| 96 |
+
def _get_state(self) -> KernelState:
|
| 97 |
+
"""Read the current state from the dataset."""
|
| 98 |
+
rec = self.records[self.current_idx + self.timestep]
|
| 99 |
+
return KernelState(
|
| 100 |
+
features=rec["state"],
|
| 101 |
+
pid=rec["pid"],
|
| 102 |
+
cpu=rec["cpu"],
|
| 103 |
+
timestep=self.timestep,
|
| 104 |
+
prev_action=self.prev_action,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
def simulate(self, state_features: list, action_value: float) -> list:
|
| 108 |
+
"""Lightweight next-state lookup for reward_fn during GRPO.
|
| 109 |
+
|
| 110 |
+
Finds the nearest recorded state in the dataset and returns
|
| 111 |
+
its recorded next_state. This is a simple approximation;
|
| 112 |
+
the World Model provides higher-fidelity simulation.
|
| 113 |
+
"""
|
| 114 |
+
import numpy as np
|
| 115 |
+
|
| 116 |
+
state_arr = np.array(state_features)
|
| 117 |
+
best_dist = float("inf")
|
| 118 |
+
best_next = state_features # fallback
|
| 119 |
+
|
| 120 |
+
# Sample a subset to keep this fast
|
| 121 |
+
sample_size = min(500, len(self.records))
|
| 122 |
+
indices = random.sample(range(len(self.records)), sample_size)
|
| 123 |
+
|
| 124 |
+
for idx in indices:
|
| 125 |
+
rec = self.records[idx]
|
| 126 |
+
dist = float(np.linalg.norm(state_arr - np.array(rec["state"])))
|
| 127 |
+
if dist < best_dist:
|
| 128 |
+
best_dist = dist
|
| 129 |
+
best_next = rec["next_state"]
|
| 130 |
+
|
| 131 |
+
return best_next
|
training/environment/rewards.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
KernelX Intelligence Layer — Multi-Objective Reward Function
|
| 3 |
+
|
| 4 |
+
Decomposes R_t = alpha * log(throughput + 1) - beta * delta_wait - gamma * |a_t - a_{t-1}|
|
| 5 |
+
into independent, inspectable reward components.
|
| 6 |
+
|
| 7 |
+
Throughput proxy: delta(sum_exec_runtime) since IPC is not yet collected
|
| 8 |
+
from PMU sidecars (indices 13-22 are zero in current data).
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
# Active feature indices (positions within the 10D active vector)
|
| 14 |
+
IDX_CPU = 0
|
| 15 |
+
IDX_PRIO = 1
|
| 16 |
+
IDX_STATIC_PRIO = 2
|
| 17 |
+
IDX_NORMAL_PRIO = 3
|
| 18 |
+
IDX_EXEC_NS = 4 # symlog-scaled sum_exec_runtime
|
| 19 |
+
IDX_VRUNTIME = 5 # symlog-scaled vruntime
|
| 20 |
+
IDX_MIGRATIONS = 6 # symlog-scaled nr_migrations
|
| 21 |
+
IDX_CPUS_ALLOWED = 7
|
| 22 |
+
IDX_CTX_SWITCHES = 8
|
| 23 |
+
IDX_WAIT_US = 9 # wait time in microseconds
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class RewardComputer:
|
| 27 |
+
"""Multi-objective reward for kernel scheduling decisions.
|
| 28 |
+
|
| 29 |
+
Components:
|
| 30 |
+
throughput — reward for CPU progress (delta exec_runtime)
|
| 31 |
+
latency — penalty for increased wait time
|
| 32 |
+
stability — penalty for jittery action changes
|
| 33 |
+
format — reward for action value in valid range
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(self, alpha: float = 1.0, beta: float = 2.0, gamma: float = 0.5):
|
| 37 |
+
self.alpha = alpha
|
| 38 |
+
self.beta = beta
|
| 39 |
+
self.gamma = gamma
|
| 40 |
+
|
| 41 |
+
def throughput_reward(self, state: list, next_state: list) -> float:
|
| 42 |
+
"""Reward for throughput improvement.
|
| 43 |
+
|
| 44 |
+
Uses delta(sum_exec_runtime) as proxy for IPC (not yet available).
|
| 45 |
+
Index 4 = sum_exec_runtime (symlog-scaled).
|
| 46 |
+
Positive delta means the process accumulated CPU time = progress.
|
| 47 |
+
"""
|
| 48 |
+
exec_delta = next_state[IDX_EXEC_NS] - state[IDX_EXEC_NS]
|
| 49 |
+
return self.alpha * float(np.log(max(0.0, exec_delta) + 1))
|
| 50 |
+
|
| 51 |
+
def latency_reward(self, state: list, next_state: list) -> float:
|
| 52 |
+
"""Penalty for increased wait time.
|
| 53 |
+
|
| 54 |
+
Index 9 = wait_time in microseconds (raw, not symlog-scaled).
|
| 55 |
+
Positive delta means wait time increased = bad.
|
| 56 |
+
"""
|
| 57 |
+
wait_delta = next_state[IDX_WAIT_US] - state[IDX_WAIT_US]
|
| 58 |
+
return -self.beta * max(0.0, wait_delta)
|
| 59 |
+
|
| 60 |
+
def stability_reward(self, action: float, prev_action: float) -> float:
|
| 61 |
+
"""Penalty for jittery scheduling changes."""
|
| 62 |
+
return -self.gamma * abs(action - prev_action)
|
| 63 |
+
|
| 64 |
+
def format_reward(self, action_value: float) -> float:
|
| 65 |
+
"""Reward for action value in valid range."""
|
| 66 |
+
return 1.0 if -1.0 <= action_value <= 1.0 else 0.0
|
| 67 |
+
|
| 68 |
+
def compute_total(
|
| 69 |
+
self,
|
| 70 |
+
state: list,
|
| 71 |
+
action, # KernelAction or has .value attribute
|
| 72 |
+
prev_action: float,
|
| 73 |
+
next_state: list,
|
| 74 |
+
) -> dict:
|
| 75 |
+
"""Compute all reward components and return breakdown."""
|
| 76 |
+
action_val = action.value if hasattr(action, "value") else float(action)
|
| 77 |
+
|
| 78 |
+
r_throughput = self.throughput_reward(state, next_state)
|
| 79 |
+
r_latency = self.latency_reward(state, next_state)
|
| 80 |
+
r_stability = self.stability_reward(action_val, prev_action)
|
| 81 |
+
r_format = self.format_reward(action_val)
|
| 82 |
+
|
| 83 |
+
total = r_throughput + r_latency + r_stability + r_format
|
| 84 |
+
|
| 85 |
+
return {
|
| 86 |
+
"total": total,
|
| 87 |
+
"throughput": r_throughput,
|
| 88 |
+
"latency": r_latency,
|
| 89 |
+
"stability": r_stability,
|
| 90 |
+
"format": r_format,
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
def calibrate(self, records: list, n_samples: int = 200) -> dict:
|
| 94 |
+
"""Run reward function on random actions to verify healthy distribution.
|
| 95 |
+
|
| 96 |
+
Returns stats about reward distribution for tuning alpha/beta/gamma.
|
| 97 |
+
"""
|
| 98 |
+
import random
|
| 99 |
+
|
| 100 |
+
totals = []
|
| 101 |
+
components = {"throughput": [], "latency": [], "stability": [], "format": []}
|
| 102 |
+
|
| 103 |
+
samples = random.sample(records, min(n_samples, len(records)))
|
| 104 |
+
for rec in samples:
|
| 105 |
+
fake_action_val = random.uniform(-1.0, 1.0)
|
| 106 |
+
fake_prev = random.uniform(-1.0, 1.0)
|
| 107 |
+
|
| 108 |
+
r_t = self.throughput_reward(rec["state"], rec["next_state"])
|
| 109 |
+
r_l = self.latency_reward(rec["state"], rec["next_state"])
|
| 110 |
+
r_s = self.stability_reward(fake_action_val, fake_prev)
|
| 111 |
+
r_f = self.format_reward(fake_action_val)
|
| 112 |
+
|
| 113 |
+
total = r_t + r_l + r_s + r_f
|
| 114 |
+
totals.append(total)
|
| 115 |
+
components["throughput"].append(r_t)
|
| 116 |
+
components["latency"].append(r_l)
|
| 117 |
+
components["stability"].append(r_s)
|
| 118 |
+
components["format"].append(r_f)
|
| 119 |
+
|
| 120 |
+
stats = {
|
| 121 |
+
"total": {
|
| 122 |
+
"mean": float(np.mean(totals)),
|
| 123 |
+
"std": float(np.std(totals)),
|
| 124 |
+
"min": float(np.min(totals)),
|
| 125 |
+
"max": float(np.max(totals)),
|
| 126 |
+
"zero_rate": float(np.mean([1 if t == 0 else 0 for t in totals])),
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
for name, vals in components.items():
|
| 130 |
+
stats[name] = {
|
| 131 |
+
"mean": float(np.mean(vals)),
|
| 132 |
+
"std": float(np.std(vals)),
|
| 133 |
+
"min": float(np.min(vals)),
|
| 134 |
+
"max": float(np.max(vals)),
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
print("\n=== Reward Calibration ===")
|
| 138 |
+
for name, s in stats.items():
|
| 139 |
+
print(f" {name:>12}: mean={s['mean']:+.3f} std={s['std']:.3f} "
|
| 140 |
+
f"range=[{s['min']:+.3f}, {s['max']:+.3f}]")
|
| 141 |
+
|
| 142 |
+
return stats
|
training/inference/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""KernelX inference engine for real-time scheduling."""
|
training/inference/benchmark_latency.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
KernelX Intelligence Layer — Latency Benchmark
|
| 3 |
+
|
| 4 |
+
Measures end-to-end inference latency of the quantized Strategist model
|
| 5 |
+
on the target CPU hardware. Reports mean, P50, P95, P99, and max latency.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python -m training.inference.benchmark_latency \
|
| 9 |
+
--model training/models/strategist_merged/strategist-q4km.gguf \
|
| 10 |
+
--samples 200
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import json
|
| 15 |
+
import sys
|
| 16 |
+
import time
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
| 22 |
+
from training.data.preprocess import FEATURE_NAMES, format_state, load_config
|
| 23 |
+
from training.inference.strategy_engine import build_inference_prompt, parse_output
|
| 24 |
+
|
| 25 |
+
CONFIG = load_config()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def benchmark(
|
| 29 |
+
model_path: str,
|
| 30 |
+
test_data_path: str = None,
|
| 31 |
+
n_samples: int = 200,
|
| 32 |
+
n_threads: int = 4,
|
| 33 |
+
temperature: float = 0.2,
|
| 34 |
+
max_tokens: int = 64,
|
| 35 |
+
warmup: int = 5,
|
| 36 |
+
):
|
| 37 |
+
"""Run latency benchmark on the quantized GGUF model."""
|
| 38 |
+
from llama_cpp import Llama
|
| 39 |
+
|
| 40 |
+
print(f"Loading model: {model_path}")
|
| 41 |
+
llm = Llama(model_path=model_path, n_ctx=512, n_threads=n_threads, verbose=False)
|
| 42 |
+
|
| 43 |
+
# Build test prompts
|
| 44 |
+
if test_data_path:
|
| 45 |
+
records = [json.loads(l) for l in open(test_data_path) if l.strip()]
|
| 46 |
+
else:
|
| 47 |
+
# Synthetic test data
|
| 48 |
+
records = []
|
| 49 |
+
for i in range(n_samples):
|
| 50 |
+
state = [float(i % 16)] # cpu
|
| 51 |
+
state += [120.0, 120.0, 120.0] # priorities
|
| 52 |
+
state += [20.0 + i * 0.1, 28.0 + i * 0.01, 8.0 + i * 0.001] # symlog'd
|
| 53 |
+
state += [16.0, float(i % 50), float(5 + i % 30)] # cpus, csw, wt_us
|
| 54 |
+
records.append({"state": state, "pid": 1000 + i, "cpu": i % 16})
|
| 55 |
+
|
| 56 |
+
records = records[:n_samples]
|
| 57 |
+
prompts = []
|
| 58 |
+
for rec in records:
|
| 59 |
+
prompts.append(build_inference_prompt(rec["state"], rec["pid"], rec["cpu"]))
|
| 60 |
+
|
| 61 |
+
# Warmup
|
| 62 |
+
print(f"Warming up ({warmup} iterations) ...")
|
| 63 |
+
for i in range(warmup):
|
| 64 |
+
llm(prompts[i % len(prompts)], max_tokens=max_tokens, temperature=temperature)
|
| 65 |
+
|
| 66 |
+
# Benchmark
|
| 67 |
+
print(f"\nBenchmarking {len(prompts)} samples ...")
|
| 68 |
+
latencies = []
|
| 69 |
+
format_ok = 0
|
| 70 |
+
token_counts = []
|
| 71 |
+
|
| 72 |
+
for prompt in prompts:
|
| 73 |
+
start = time.perf_counter()
|
| 74 |
+
output = llm(prompt, max_tokens=max_tokens, temperature=temperature)
|
| 75 |
+
elapsed = time.perf_counter() - start
|
| 76 |
+
latencies.append(elapsed)
|
| 77 |
+
|
| 78 |
+
text = output["choices"][0]["text"]
|
| 79 |
+
tokens = output["usage"]["completion_tokens"]
|
| 80 |
+
token_counts.append(tokens)
|
| 81 |
+
|
| 82 |
+
action_val = parse_output(text)
|
| 83 |
+
if -1.0 <= action_val <= 1.0:
|
| 84 |
+
format_ok += 1
|
| 85 |
+
|
| 86 |
+
latencies_ms = np.array(latencies) * 1000
|
| 87 |
+
|
| 88 |
+
# Report
|
| 89 |
+
target = CONFIG["model"]["target_inference_ms"]
|
| 90 |
+
print(f"\n{'='*50}")
|
| 91 |
+
print(f" KernelX Latency Benchmark")
|
| 92 |
+
print(f"{'='*50}")
|
| 93 |
+
print(f" Model: {Path(model_path).name}")
|
| 94 |
+
print(f" Threads: {n_threads}")
|
| 95 |
+
print(f" Samples: {len(prompts)}")
|
| 96 |
+
print(f" Target: <{target}ms")
|
| 97 |
+
print(f"{'='*50}")
|
| 98 |
+
print(f" Mean: {np.mean(latencies_ms):>8.1f} ms")
|
| 99 |
+
print(f" Median: {np.median(latencies_ms):>8.1f} ms")
|
| 100 |
+
print(f" P95: {np.percentile(latencies_ms, 95):>8.1f} ms")
|
| 101 |
+
print(f" P99: {np.percentile(latencies_ms, 99):>8.1f} ms")
|
| 102 |
+
print(f" Max: {np.max(latencies_ms):>8.1f} ms")
|
| 103 |
+
print(f" Min: {np.min(latencies_ms):>8.1f} ms")
|
| 104 |
+
print(f" Std: {np.std(latencies_ms):>8.1f} ms")
|
| 105 |
+
print(f"{'='*50}")
|
| 106 |
+
print(f" Tokens/s: {np.sum(token_counts) / np.sum(latencies):>8.1f}")
|
| 107 |
+
print(f" Format OK:{format_ok}/{len(prompts)} ({format_ok/len(prompts)*100:.1f}%)")
|
| 108 |
+
print(f"{'='*50}")
|
| 109 |
+
|
| 110 |
+
p95 = np.percentile(latencies_ms, 95)
|
| 111 |
+
if p95 <= target:
|
| 112 |
+
print(f" VERDICT: PASS (P95 {p95:.1f}ms <= {target}ms)")
|
| 113 |
+
else:
|
| 114 |
+
print(f" VERDICT: FAIL (P95 {p95:.1f}ms > {target}ms)")
|
| 115 |
+
|
| 116 |
+
return {
|
| 117 |
+
"mean_ms": float(np.mean(latencies_ms)),
|
| 118 |
+
"p95_ms": float(p95),
|
| 119 |
+
"p99_ms": float(np.percentile(latencies_ms, 99)),
|
| 120 |
+
"format_ok_pct": format_ok / len(prompts) * 100,
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def main():
|
| 125 |
+
parser = argparse.ArgumentParser(description="Benchmark KernelX inference latency")
|
| 126 |
+
parser.add_argument("--model", required=True, help="Path to GGUF model")
|
| 127 |
+
parser.add_argument("--test-data", default=None, help="Test JSONL (optional)")
|
| 128 |
+
parser.add_argument("--samples", type=int, default=200)
|
| 129 |
+
parser.add_argument("--threads", type=int, default=4)
|
| 130 |
+
parser.add_argument("--temperature", type=float, default=0.2)
|
| 131 |
+
parser.add_argument("--max-tokens", type=int, default=8)
|
| 132 |
+
parser.add_argument("--warmup", type=int, default=5)
|
| 133 |
+
args = parser.parse_args()
|
| 134 |
+
|
| 135 |
+
benchmark(
|
| 136 |
+
model_path=args.model,
|
| 137 |
+
test_data_path=args.test_data,
|
| 138 |
+
n_samples=args.samples,
|
| 139 |
+
n_threads=args.threads,
|
| 140 |
+
temperature=args.temperature,
|
| 141 |
+
max_tokens=args.max_tokens,
|
| 142 |
+
warmup=args.warmup,
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
if __name__ == "__main__":
|
| 147 |
+
main()
|
training/inference/strategy_engine.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
KernelX Intelligence Layer — Inference Engine (Stage 6)
|
| 3 |
+
|
| 4 |
+
Three-thread architecture for real-time kernel scheduling:
|
| 5 |
+
Thread 1 (Telemetry): reads latest state from shared memory
|
| 6 |
+
Thread 2 (Strategist): runs LLM inference every cycle
|
| 7 |
+
Thread 3 (Updater): writes action to shared memory command slot
|
| 8 |
+
|
| 9 |
+
Uses llama.cpp via llama-cpp-python for sub-50ms CPU inference
|
| 10 |
+
with the quantized GGUF Strategist model.
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
python -m training.inference.strategy_engine \
|
| 14 |
+
--model training/models/strategist_merged/strategist-q4km.gguf \
|
| 15 |
+
--shm-path /dev/shm/kernelx_state
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import json
|
| 20 |
+
import mmap
|
| 21 |
+
import os
|
| 22 |
+
import re
|
| 23 |
+
import struct
|
| 24 |
+
import sys
|
| 25 |
+
import threading
|
| 26 |
+
import time
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
|
| 31 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
| 32 |
+
from training.data.preprocess import (
|
| 33 |
+
ACTIVE_FEATURES, FEATURE_NAMES, SYMLOG_FEATURES,
|
| 34 |
+
symmetric_log, format_state, load_config,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
CONFIG = load_config()
|
| 38 |
+
|
| 39 |
+
# ---------------------------------------------------------------------------
|
| 40 |
+
# Shared memory layout (must match bridge/src/main.rs HUDState)
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# features: [u64; 24] = 192 bytes (offset 0)
|
| 43 |
+
# current_action: f32 = 4 bytes (offset 192)
|
| 44 |
+
# active_pid: u32 = 4 bytes (offset 196)
|
| 45 |
+
# is_clamped: u32 = 4 bytes (offset 200)
|
| 46 |
+
# reasoning: [u8; 128] = 128 bytes (offset 204)
|
| 47 |
+
# p99_wait_us: u64 = 8 bytes (offset 332)
|
| 48 |
+
# Total: 340 bytes
|
| 49 |
+
|
| 50 |
+
SHM_SIZE = 340
|
| 51 |
+
FEATURES_OFFSET = 0
|
| 52 |
+
FEATURES_SIZE = 192 # 24 * 8 bytes (u64)
|
| 53 |
+
ACTION_OFFSET = 192
|
| 54 |
+
PID_OFFSET = 196
|
| 55 |
+
CLAMPED_OFFSET = 200
|
| 56 |
+
REASONING_OFFSET = 204
|
| 57 |
+
REASONING_SIZE = 128
|
| 58 |
+
P99_OFFSET = 332
|
| 59 |
+
|
| 60 |
+
# ---------------------------------------------------------------------------
|
| 61 |
+
# Prompt building (mirrors train_strategist.py)
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
+
|
| 64 |
+
def build_inference_prompt(active_features: list, pid: int, cpu: int) -> str:
|
| 65 |
+
state_str = format_state(active_features)
|
| 66 |
+
return (
|
| 67 |
+
"<|system|>You are a Linux kernel scheduling strategist. "
|
| 68 |
+
"Given the current system state, output a scheduling action.<|end|>\n"
|
| 69 |
+
f"<|user|>[STATE] {state_str}\n"
|
| 70 |
+
f"[PID] {pid} [CPU] {cpu}\n"
|
| 71 |
+
"[ACTION]<|end|>\n"
|
| 72 |
+
"<|assistant|>"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def parse_output(text: str) -> float:
|
| 77 |
+
"""Parse action float from model output."""
|
| 78 |
+
action_match = re.search(r"\[ACTION\]\s*([-+]?\d*\.?\d+)", text)
|
| 79 |
+
if not action_match:
|
| 80 |
+
action_match = re.search(r"([-+]?\d*\.?\d+)", text)
|
| 81 |
+
if not action_match:
|
| 82 |
+
return 0.0
|
| 83 |
+
|
| 84 |
+
action_val = float(action_match.group(1))
|
| 85 |
+
return max(-1.0, min(1.0, action_val))
|
| 86 |
+
|
| 87 |
+
# ---------------------------------------------------------------------------
|
| 88 |
+
# Shared memory reader/writer
|
| 89 |
+
# ---------------------------------------------------------------------------
|
| 90 |
+
|
| 91 |
+
def read_features_from_shm(shm: mmap.mmap) -> tuple:
|
| 92 |
+
"""Read 24D raw features + PID from shared memory.
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
(raw_features_24d: list[float], pid: int, cpu: int)
|
| 96 |
+
"""
|
| 97 |
+
shm.seek(FEATURES_OFFSET)
|
| 98 |
+
raw_bytes = shm.read(FEATURES_SIZE)
|
| 99 |
+
raw_features = list(np.frombuffer(raw_bytes, dtype=np.uint64).astype(np.float64))
|
| 100 |
+
|
| 101 |
+
shm.seek(PID_OFFSET)
|
| 102 |
+
pid = struct.unpack("<I", shm.read(4))[0]
|
| 103 |
+
|
| 104 |
+
# CPU is in features[0] (from bpf_get_smp_processor_id)
|
| 105 |
+
cpu = int(raw_features[0]) if raw_features else 0
|
| 106 |
+
|
| 107 |
+
return raw_features, pid, cpu
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def preprocess_for_inference(raw_features: list) -> list:
|
| 111 |
+
"""Apply symlog scaling and extract active features for the LLM."""
|
| 112 |
+
f = list(raw_features)
|
| 113 |
+
for idx in SYMLOG_FEATURES:
|
| 114 |
+
f[idx] = symmetric_log(f[idx])
|
| 115 |
+
return [f[i] for i in ACTIVE_FEATURES]
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def write_action_to_shm(shm: mmap.mmap, action: float):
|
| 119 |
+
"""Write action value to shared memory."""
|
| 120 |
+
shm.seek(ACTION_OFFSET)
|
| 121 |
+
shm.write(struct.pack("<f", action))
|
| 122 |
+
|
| 123 |
+
# ---------------------------------------------------------------------------
|
| 124 |
+
# Strategy Engine (three-thread architecture)
|
| 125 |
+
# ---------------------------------------------------------------------------
|
| 126 |
+
|
| 127 |
+
class StrategyEngine:
|
| 128 |
+
"""Real-time scheduling inference engine.
|
| 129 |
+
|
| 130 |
+
Reads kernel state from shared memory, runs the quantized Strategist
|
| 131 |
+
model, and writes the scheduling action back.
|
| 132 |
+
"""
|
| 133 |
+
|
| 134 |
+
def __init__(
|
| 135 |
+
self,
|
| 136 |
+
model_path: str,
|
| 137 |
+
shm_path: str = "/dev/shm/kernelx_state",
|
| 138 |
+
n_threads: int = 2,
|
| 139 |
+
poll_interval_ms: float = 10.0,
|
| 140 |
+
update_interval_ms: float = 50.0,
|
| 141 |
+
temperature: float = 0.2,
|
| 142 |
+
max_tokens: int = 8,
|
| 143 |
+
):
|
| 144 |
+
from llama_cpp import Llama
|
| 145 |
+
|
| 146 |
+
self.model_path = model_path
|
| 147 |
+
self.shm_path = shm_path
|
| 148 |
+
self.poll_interval = poll_interval_ms / 1000.0
|
| 149 |
+
self.update_interval = update_interval_ms / 1000.0
|
| 150 |
+
self.temperature = temperature
|
| 151 |
+
self.max_tokens = max_tokens
|
| 152 |
+
|
| 153 |
+
# Load model
|
| 154 |
+
print(f"[StrategyEngine] Loading model: {model_path}")
|
| 155 |
+
self.llm = Llama(
|
| 156 |
+
model_path=model_path,
|
| 157 |
+
n_ctx=512,
|
| 158 |
+
n_threads=n_threads,
|
| 159 |
+
verbose=False,
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Shared state (protected by lock)
|
| 163 |
+
self.lock = threading.Lock()
|
| 164 |
+
self.current_features = [0.0] * len(ACTIVE_FEATURES)
|
| 165 |
+
self.current_pid = 0
|
| 166 |
+
self.current_cpu = 0
|
| 167 |
+
self.latest_action = 0.0
|
| 168 |
+
|
| 169 |
+
# Control
|
| 170 |
+
self.running = False
|
| 171 |
+
self.shm = None
|
| 172 |
+
|
| 173 |
+
# Metrics
|
| 174 |
+
self.inference_count = 0
|
| 175 |
+
self.inference_latencies = []
|
| 176 |
+
|
| 177 |
+
def _open_shm(self) -> mmap.mmap:
|
| 178 |
+
"""Open shared memory file for read/write."""
|
| 179 |
+
fd = os.open(self.shm_path, os.O_RDWR)
|
| 180 |
+
return mmap.mmap(fd, SHM_SIZE, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)
|
| 181 |
+
|
| 182 |
+
def start(self):
|
| 183 |
+
"""Start all three threads."""
|
| 184 |
+
if not os.path.exists(self.shm_path):
|
| 185 |
+
print(f"[StrategyEngine] WARNING: SHM {self.shm_path} not found.")
|
| 186 |
+
print("[StrategyEngine] Running in dry-run mode (no SHM I/O).")
|
| 187 |
+
self.shm = None
|
| 188 |
+
else:
|
| 189 |
+
self.shm = self._open_shm()
|
| 190 |
+
print(f"[StrategyEngine] Connected to SHM: {self.shm_path}")
|
| 191 |
+
|
| 192 |
+
self.running = True
|
| 193 |
+
|
| 194 |
+
threads = [
|
| 195 |
+
threading.Thread(target=self._telemetry_loop, name="telemetry", daemon=True),
|
| 196 |
+
threading.Thread(target=self._strategist_loop, name="strategist", daemon=True),
|
| 197 |
+
threading.Thread(target=self._update_loop, name="updater", daemon=True),
|
| 198 |
+
]
|
| 199 |
+
|
| 200 |
+
for t in threads:
|
| 201 |
+
t.start()
|
| 202 |
+
print(f"[StrategyEngine] Started {t.name} thread")
|
| 203 |
+
|
| 204 |
+
print("[StrategyEngine] All threads running. Press Ctrl+C to stop.")
|
| 205 |
+
|
| 206 |
+
try:
|
| 207 |
+
while self.running:
|
| 208 |
+
time.sleep(1.0)
|
| 209 |
+
# Periodic stats
|
| 210 |
+
if self.inference_count > 0 and self.inference_count % 20 == 0:
|
| 211 |
+
recent = self.inference_latencies[-20:]
|
| 212 |
+
avg_ms = np.mean(recent) * 1000
|
| 213 |
+
print(f"[StrategyEngine] Inferences: {self.inference_count}, "
|
| 214 |
+
f"Avg latency: {avg_ms:.1f}ms, "
|
| 215 |
+
f"Action: {self.latest_action:.4f}")
|
| 216 |
+
except KeyboardInterrupt:
|
| 217 |
+
self.stop()
|
| 218 |
+
|
| 219 |
+
def stop(self):
|
| 220 |
+
"""Stop all threads."""
|
| 221 |
+
print("\n[StrategyEngine] Shutting down ...")
|
| 222 |
+
self.running = False
|
| 223 |
+
if self.shm:
|
| 224 |
+
self.shm.close()
|
| 225 |
+
|
| 226 |
+
def _telemetry_loop(self):
|
| 227 |
+
"""Thread 1: Read latest state from shared memory."""
|
| 228 |
+
while self.running:
|
| 229 |
+
if self.shm:
|
| 230 |
+
try:
|
| 231 |
+
raw_features, pid, cpu = read_features_from_shm(self.shm)
|
| 232 |
+
active = preprocess_for_inference(raw_features)
|
| 233 |
+
with self.lock:
|
| 234 |
+
self.current_features = active
|
| 235 |
+
self.current_pid = pid
|
| 236 |
+
self.current_cpu = cpu
|
| 237 |
+
except Exception:
|
| 238 |
+
pass
|
| 239 |
+
time.sleep(self.poll_interval)
|
| 240 |
+
|
| 241 |
+
def _strategist_loop(self):
|
| 242 |
+
"""Thread 2: Run LLM inference every cycle."""
|
| 243 |
+
while self.running:
|
| 244 |
+
with self.lock:
|
| 245 |
+
features = list(self.current_features)
|
| 246 |
+
pid = self.current_pid
|
| 247 |
+
cpu = self.current_cpu
|
| 248 |
+
|
| 249 |
+
prompt = build_inference_prompt(features, pid, cpu)
|
| 250 |
+
|
| 251 |
+
start = time.perf_counter()
|
| 252 |
+
output = self.llm(prompt, max_tokens=self.max_tokens,
|
| 253 |
+
temperature=self.temperature)
|
| 254 |
+
elapsed = time.perf_counter() - start
|
| 255 |
+
|
| 256 |
+
text = output["choices"][0]["text"]
|
| 257 |
+
action = parse_output(text)
|
| 258 |
+
|
| 259 |
+
with self.lock:
|
| 260 |
+
self.latest_action = action
|
| 261 |
+
|
| 262 |
+
self.inference_count += 1
|
| 263 |
+
self.inference_latencies.append(elapsed)
|
| 264 |
+
|
| 265 |
+
def _update_loop(self):
|
| 266 |
+
"""Thread 3: Write action to shared memory command slot."""
|
| 267 |
+
while self.running:
|
| 268 |
+
with self.lock:
|
| 269 |
+
action = self.latest_action
|
| 270 |
+
|
| 271 |
+
if self.shm:
|
| 272 |
+
try:
|
| 273 |
+
write_action_to_shm(self.shm, action)
|
| 274 |
+
except Exception:
|
| 275 |
+
pass
|
| 276 |
+
|
| 277 |
+
time.sleep(self.update_interval)
|
| 278 |
+
|
| 279 |
+
# ---------------------------------------------------------------------------
|
| 280 |
+
# CLI
|
| 281 |
+
# ---------------------------------------------------------------------------
|
| 282 |
+
|
| 283 |
+
def main():
|
| 284 |
+
parser = argparse.ArgumentParser(description="KernelX Strategy Engine")
|
| 285 |
+
parser.add_argument("--model", required=True, help="Path to GGUF model")
|
| 286 |
+
parser.add_argument("--shm-path", default="/dev/shm/kernelx_state")
|
| 287 |
+
parser.add_argument("--threads", type=int, default=2, help="llama.cpp threads")
|
| 288 |
+
parser.add_argument("--poll-ms", type=float, default=10.0)
|
| 289 |
+
parser.add_argument("--update-ms", type=float, default=50.0)
|
| 290 |
+
parser.add_argument("--temperature", type=float, default=0.2)
|
| 291 |
+
parser.add_argument("--max-tokens", type=int, default=64)
|
| 292 |
+
args = parser.parse_args()
|
| 293 |
+
|
| 294 |
+
engine = StrategyEngine(
|
| 295 |
+
model_path=args.model,
|
| 296 |
+
shm_path=args.shm_path,
|
| 297 |
+
n_threads=args.threads,
|
| 298 |
+
poll_interval_ms=args.poll_ms,
|
| 299 |
+
update_interval_ms=args.update_ms,
|
| 300 |
+
temperature=args.temperature,
|
| 301 |
+
max_tokens=args.max_tokens,
|
| 302 |
+
)
|
| 303 |
+
engine.start()
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
if __name__ == "__main__":
|
| 307 |
+
main()
|
training/requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.0
|
| 2 |
+
transformers>=4.40
|
| 3 |
+
trl>=0.12
|
| 4 |
+
unsloth
|
| 5 |
+
peft>=0.13
|
| 6 |
+
datasets>=2.18
|
| 7 |
+
accelerate>=0.30
|
| 8 |
+
llama-cpp-python>=0.3
|
| 9 |
+
wandb
|
| 10 |
+
pandas
|
| 11 |
+
numpy
|
| 12 |
+
gradio>=4.0
|
training/run_pipeline.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
KernelX Intelligence Layer — Full Training Pipeline Runner
|
| 4 |
+
|
| 5 |
+
Runs all stages end-to-end: preprocess -> World Model SFT -> Strategist GRPO -> export.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
# Full pipeline
|
| 9 |
+
python training/run_pipeline.py \
|
| 10 |
+
--raw-data data/state_transitions.jsonl \
|
| 11 |
+
--output-root training
|
| 12 |
+
|
| 13 |
+
# Resume from a specific stage
|
| 14 |
+
python training/run_pipeline.py \
|
| 15 |
+
--raw-data data/state_transitions.jsonl \
|
| 16 |
+
--output-root training \
|
| 17 |
+
--start-stage 3
|
| 18 |
+
|
| 19 |
+
Stages:
|
| 20 |
+
1 = Preprocess data
|
| 21 |
+
2 = Train World Model (SFT)
|
| 22 |
+
3 = Train Strategist (warm-start SFT + GRPO)
|
| 23 |
+
4 = Export & quantize to GGUF
|
| 24 |
+
5 = Validate quantized model
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
import argparse
|
| 28 |
+
import json
|
| 29 |
+
import sys
|
| 30 |
+
from pathlib import Path
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
parser = argparse.ArgumentParser(description="KernelX full training pipeline")
|
| 34 |
+
parser.add_argument("--raw-data", required=True, help="Path to raw state_transitions.jsonl")
|
| 35 |
+
parser.add_argument("--output-root", default="training", help="Root output directory")
|
| 36 |
+
parser.add_argument("--start-stage", type=int, default=1, help="Stage to start from (1-5)")
|
| 37 |
+
parser.add_argument("--end-stage", type=int, default=5, help="Stage to end at (1-5)")
|
| 38 |
+
parser.add_argument("--epochs-world", type=int, default=3, help="World Model training epochs")
|
| 39 |
+
parser.add_argument("--epochs-strategist", type=int, default=3, help="Strategist GRPO epochs")
|
| 40 |
+
parser.add_argument("--batch-size", type=int, default=4)
|
| 41 |
+
parser.add_argument("--quantize", default="Q4_K_M")
|
| 42 |
+
parser.add_argument("--wandb", action="store_true")
|
| 43 |
+
parser.add_argument("--curriculum", action="store_true")
|
| 44 |
+
args = parser.parse_args()
|
| 45 |
+
|
| 46 |
+
root = Path(args.output_root)
|
| 47 |
+
data_dir = root / "data"
|
| 48 |
+
models_dir = root / "models"
|
| 49 |
+
|
| 50 |
+
train_path = data_dir / "train.jsonl"
|
| 51 |
+
val_path = data_dir / "val.jsonl"
|
| 52 |
+
test_path = data_dir / "test.jsonl"
|
| 53 |
+
|
| 54 |
+
# ------------------------------------------------------------------
|
| 55 |
+
# Stage 1: Preprocess
|
| 56 |
+
# ------------------------------------------------------------------
|
| 57 |
+
if args.start_stage <= 1 <= args.end_stage:
|
| 58 |
+
print("\n" + "=" * 60)
|
| 59 |
+
print(" STAGE 1: Data Preprocessing")
|
| 60 |
+
print("=" * 60)
|
| 61 |
+
|
| 62 |
+
from training.data.preprocess import run_pipeline
|
| 63 |
+
run_pipeline(args.raw_data, str(data_dir), audit=True)
|
| 64 |
+
|
| 65 |
+
# ------------------------------------------------------------------
|
| 66 |
+
# Stage 2: World Model SFT
|
| 67 |
+
# ------------------------------------------------------------------
|
| 68 |
+
if args.start_stage <= 2 <= args.end_stage:
|
| 69 |
+
print("\n" + "=" * 60)
|
| 70 |
+
print(" STAGE 2: World Model Training (SFT)")
|
| 71 |
+
print("=" * 60)
|
| 72 |
+
|
| 73 |
+
from training.models.train_world_model import train, evaluate_world_model
|
| 74 |
+
|
| 75 |
+
wm_dir = models_dir / "world_model_final"
|
| 76 |
+
model, tokenizer = train(
|
| 77 |
+
train_path=str(train_path),
|
| 78 |
+
val_path=str(val_path),
|
| 79 |
+
output_dir=str(wm_dir),
|
| 80 |
+
num_epochs=args.epochs_world,
|
| 81 |
+
batch_size=args.batch_size,
|
| 82 |
+
use_wandb=args.wandb,
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
if test_path.exists():
|
| 86 |
+
evaluate_world_model(model, tokenizer, str(test_path))
|
| 87 |
+
|
| 88 |
+
# ------------------------------------------------------------------
|
| 89 |
+
# Stage 3: Strategist Training (warm-start + GRPO)
|
| 90 |
+
# ------------------------------------------------------------------
|
| 91 |
+
if args.start_stage <= 3 <= args.end_stage:
|
| 92 |
+
print("\n" + "=" * 60)
|
| 93 |
+
print(" STAGE 3: Strategist Training (GRPO)")
|
| 94 |
+
print("=" * 60)
|
| 95 |
+
|
| 96 |
+
from training.models.train_strategist import (
|
| 97 |
+
run_warmstart, run_grpo, inspect_generations,
|
| 98 |
+
)
|
| 99 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 100 |
+
|
| 101 |
+
records = [json.loads(l) for l in open(train_path) if l.strip()]
|
| 102 |
+
|
| 103 |
+
# Phase 1: Warm-start
|
| 104 |
+
ws_dir = models_dir / "strategist_warmstart"
|
| 105 |
+
model, tokenizer = run_warmstart(
|
| 106 |
+
records=records,
|
| 107 |
+
output_dir=str(ws_dir),
|
| 108 |
+
use_wandb=args.wandb,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# Phase 2: GRPO
|
| 112 |
+
strat_dir = models_dir / "strategist_final"
|
| 113 |
+
model, tokenizer = run_grpo(
|
| 114 |
+
model=model,
|
| 115 |
+
tokenizer=tokenizer,
|
| 116 |
+
train_records=records,
|
| 117 |
+
output_dir=str(strat_dir),
|
| 118 |
+
num_epochs=args.epochs_strategist,
|
| 119 |
+
use_curriculum=args.curriculum,
|
| 120 |
+
use_wandb=args.wandb,
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
# Inspect
|
| 124 |
+
inspect_generations(model, tokenizer, records, n=10)
|
| 125 |
+
|
| 126 |
+
# ------------------------------------------------------------------
|
| 127 |
+
# Stage 4: Export & Quantize
|
| 128 |
+
# ------------------------------------------------------------------
|
| 129 |
+
if args.start_stage <= 4 <= args.end_stage:
|
| 130 |
+
print("\n" + "=" * 60)
|
| 131 |
+
print(" STAGE 4: Export & Quantize to GGUF")
|
| 132 |
+
print("=" * 60)
|
| 133 |
+
|
| 134 |
+
from training.models.export_gguf import merge_lora, convert_to_gguf
|
| 135 |
+
|
| 136 |
+
strat_dir = models_dir / "strategist_final"
|
| 137 |
+
merged_dir = models_dir / "strategist_merged"
|
| 138 |
+
merged_path = merge_lora(str(strat_dir), str(merged_dir))
|
| 139 |
+
convert_to_gguf(merged_path, str(merged_dir), args.quantize)
|
| 140 |
+
|
| 141 |
+
# ------------------------------------------------------------------
|
| 142 |
+
# Stage 5: Validate
|
| 143 |
+
# ------------------------------------------------------------------
|
| 144 |
+
if args.start_stage <= 5 <= args.end_stage:
|
| 145 |
+
print("\n" + "=" * 60)
|
| 146 |
+
print(" STAGE 5: Validate Quantized Model")
|
| 147 |
+
print("=" * 60)
|
| 148 |
+
|
| 149 |
+
from training.models.export_gguf import validate_gguf
|
| 150 |
+
|
| 151 |
+
quant_name = f"strategist-{args.quantize.lower().replace('_', '')}.gguf"
|
| 152 |
+
gguf_path = models_dir / "strategist_merged" / quant_name
|
| 153 |
+
|
| 154 |
+
if gguf_path.exists():
|
| 155 |
+
validate_gguf(str(gguf_path), str(test_path) if test_path.exists() else None)
|
| 156 |
+
else:
|
| 157 |
+
print(f"GGUF not found at {gguf_path}. Run stage 4 first.")
|
| 158 |
+
|
| 159 |
+
# ------------------------------------------------------------------
|
| 160 |
+
print("\n" + "=" * 60)
|
| 161 |
+
print(" Pipeline complete!")
|
| 162 |
+
print("=" * 60)
|
| 163 |
+
print(f"\nArtifacts in: {root}/")
|
| 164 |
+
print(f" Data: {data_dir}/")
|
| 165 |
+
print(f" Models: {models_dir}/")
|
| 166 |
+
print(f"\nNext steps:")
|
| 167 |
+
print(f" 1. Run demo: python -m training.demo.app --test-data {test_path}")
|
| 168 |
+
print(f" 2. Run engine: python -m training.inference.strategy_engine --model <gguf>")
|
| 169 |
+
print(f" 3. Benchmark: python -m training.inference.benchmark_latency --model <gguf>")
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
if __name__ == "__main__":
|
| 173 |
+
main()
|