Rayugacodes commited on
Commit
32a197f
·
verified ·
1 Parent(s): 572c16e

Training pipeline scripts

Browse files
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()