Download code/train_noema_predictor.py from BinSaqban/rushd-agent: direct link, hf CLI and curl.
- Browser
- Download file 9.94 kB
-
https://huggingface.co/BinSaqban/rushd-agent/resolve/main/code/train_noema_predictor.py
- Command line
-
hf download hf://BinSaqban/rushd-agent/code/train_noema_predictor.py
-
curl -L -o train_noema_predictor.py https://huggingface.co/BinSaqban/rushd-agent/resolve/main/code/train_noema_predictor.py
9.94 kB
| """Train Noema Predictor for Rushd-Geo Early Exit | |
| MLP(4096 → 4096) predicts L48 hidden state from L6 | |
| Architecture: | |
| L1 → L6 (concept phase, compute NCI) | |
| L6 → [MLP Predictor] → predicted L48 | |
| predicted L48 → L48-L63 (final layers) | |
| → norm + lm_head → output | |
| Training: collect (L6_hs, L48_hs) pairs, train MLP with MSE | |
| """ | |
| import os, sys, json, time, math | |
| os.environ["WANDB_DISABLED"] = "true" | |
| import mlx.core as mx | |
| import mlx.nn as nn | |
| import mlx.optimizers as optim | |
| from mlx_lm import load | |
| import numpy as np | |
| MODEL = "/Users/ai/rushd-geo-mlx-4bit" | |
| N_LAYERS = 64 | |
| EXIT_LAYER = 6 | |
| TARGET_LAYER = 48 | |
| print("=" * 60) | |
| print("🚀 Training Noema Predictor for Rushd-Geo Early Exit") | |
| print("=" * 60, flush=True) | |
| # Load model | |
| print("\nLoading Rushd-Geo...", flush=True) | |
| t0 = time.time() | |
| model, tokenizer = load(MODEL) | |
| print(f"Loaded in {time.time()-t0:.1f}s, {len(model.layers)} layers", flush=True) | |
| HIDDEN = 5120 # Rushd-Geo hidden dim | |
| # Monkey-patch to capture hidden states | |
| from mlx_lm.models.qwen3_5 import DecoderLayer | |
| original_call = DecoderLayer.__call__ | |
| def create_collector(): | |
| """Returns (collecting function, result dict)""" | |
| layer_idx = [0] | |
| collected = {"layer_6": None, "layer_48": None} | |
| def collect(self, x, mask=None, cache=None): | |
| i = layer_idx[0] | |
| layer_idx[0] += 1 | |
| result = original_call(self, x, mask=mask, cache=cache) | |
| if i == EXIT_LAYER: | |
| collected["layer_6"] = result | |
| elif i == TARGET_LAYER: | |
| collected["layer_48"] = result | |
| return result | |
| return collect, collected | |
| # Training data texts — diverse Arabic | |
| TRAIN_TEXTS = [ | |
| # Political/Economic | |
| "تحليل الوضع الجيوسياسي في الشرق الأوسط بعد اتفاقيات التطبيع وتأثيرها على أسعار النفط.", | |
| "العلاقات الأمريكية الصينية في ظل الحرب التجارية وتأثيرها على الاقتصاد العالمي.", | |
| "أزمة الطاقة في أوروبا بعد العقوبات على روسيا والبحث عن بدائل.", | |
| "التحول نحو الطاقة المتجددة في دول الخليج ورؤية 2030.", | |
| "التوترات في مضيق تايوان وتأثيرها على الأمن الإقليمي.", | |
| # Historical/Cultural | |
| "تاريخ الحضارة الإسلامية في الأندلس ودورها في نقل العلوم إلى أوروبا.", | |
| "تأثير العولمة على الهوية الثقافية في المجتمعات العربية.", | |
| "دور الترجمة في نقل المعرفة بين الحضارات عبر التاريخ.", | |
| "النهضة العلمية في العصر العباسي ودور بيت الحكمة.", | |
| "المخطوطات العربية وأهميتها في حفظ التراث العلمي العالمي.", | |
| # Scientific/Technical | |
| "تطور الذكاء الاصطناعي وتأثيره على سوق العمل في المستقبل.", | |
| "الحوسبة الكمومية: المبادئ الأساسية والتطبيقات المستقبلية.", | |
| "تقنية البلوكشين والعملات الرقمية: فرص وتحديات.", | |
| "الطائرات بدون طيار وتطبيقاتها في المجال المدني والعسكري.", | |
| "تقنيات تحلية المياه ودورها في مواجهة أزمة المياه في الشرق الأوسط.", | |
| # Philosophical | |
| "العلاقة بين العقل والنفس في الفلسفة الإسلامية والعصرية.", | |
| "نظرية المعرفة: مصادر المعرفة الإنسانية ومحدوديتها.", | |
| "الأخلاق في عصر التكنولوجيا: التحديات والمسؤوليات.", | |
| "مفهوم الحرية بين الفلسفة الغربية والإسلامية.", | |
| "العدالة الاجتماعية في الفكر السياسي المعاصر.", | |
| # Short/Simple | |
| "السماء زرقاء لأن الضوء الأزرق يتشتت أكثر من غيره.", | |
| "الماء ضروري للحياة وجميع الكائنات الحية تحتاج إليه.", | |
| "التعليم هو أساس تقدم المجتمعات ورفاهيتها.", | |
| "الصحة هي الثروة الحقيقية للإنسان.", | |
| "الوقت كالسيف إن لم تقطعه قطعك.", | |
| ] | |
| print(f"\n📚 Collecting {len(TRAIN_TEXTS)} training samples...", flush=True) | |
| print(f" Each sample: {HIDDEN} dim at L{EXIT_LAYER} and L{TARGET_LAYER}", flush=True) | |
| X_data = [] # L6 features | |
| Y_data = [] # L48 targets | |
| for i, text in enumerate(TRAIN_TEXTS): | |
| tokens = list(tokenizer.encode(text)) | |
| # Ensure reasonable length | |
| tokens = tokens[:512] # Cap at 512 tokens | |
| if len(tokens) < 4: | |
| continue | |
| input_ids = mx.array([tokens]) | |
| # Collect hidden states | |
| collect_fn, collected = create_collector() | |
| DecoderLayer.__call__ = collect_fn | |
| try: | |
| logits = model(input_ids) | |
| DecoderLayer.__call__ = original_call | |
| if collected["layer_6"] is not None and collected["layer_48"] is not None: | |
| # Mean pool over sequence dimension | |
| l6 = collected["layer_6"].mean(axis=1).squeeze(0) # [5120] | |
| l48 = collected["layer_48"].mean(axis=1).squeeze(0) # [5120] | |
| X_data.append(l6) | |
| Y_data.append(l48) | |
| # Compute NCI for info | |
| nci = float(mx.sum(l6 * l48) / (mx.linalg.norm(l6) * mx.linalg.norm(l48))) | |
| print(f" [{i+1}/{len(TRAIN_TEXTS)}] NCI(L{EXIT_LAYER},L{TARGET_LAYER})={nci:.4f}", flush=True) | |
| else: | |
| print(f" [{i+1}/{len(TRAIN_TEXTS)}] MISSING states", flush=True) | |
| except Exception as e: | |
| DecoderLayer.__call__ = original_call | |
| print(f" [{i+1}/{len(TRAIN_TEXTS)}] ERROR: {str(e)[:50]}", flush=True) | |
| print(f"\n✅ Collected {len(X_data)} (L6, L48) pairs", flush=True) | |
| if len(X_data) < 5: | |
| print("❌ Not enough data to train! Need at least 5 pairs.", flush=True) | |
| sys.exit(1) | |
| # Convert to MLX arrays | |
| X = mx.stack(X_data) # [N, 5120] | |
| Y = mx.stack(Y_data) # [N, 5120] | |
| print(f"\nX shape: {X.shape}, Y shape: {Y.shape}", flush=True) | |
| # Define MLP Predictor | |
| class NoemaPredictor(nn.Module): | |
| def __init__(self, dim=5120, hidden_dim=None): | |
| super().__init__() | |
| if hidden_dim is None: | |
| hidden_dim = max(dim // 2, 64) | |
| self.net = nn.Sequential( | |
| nn.Linear(dim, hidden_dim), | |
| nn.ReLU(), | |
| nn.Linear(hidden_dim, dim), | |
| ) | |
| def __call__(self, x): | |
| return self.net(x) | |
| # Initialize | |
| predictor = NoemaPredictor(dim=HIDDEN, hidden_dim=2048) | |
| print(f"\n🧠 MLP architecture:", flush=True) | |
| print(f" Linear({HIDDEN} → 2048)", flush=True) | |
| print(f" ReLU", flush=True) | |
| print(f" Linear(2048 → {HIDDEN})", flush=True) | |
| total_params = HIDDEN * 2048 + 2048 + 2048 * HIDDEN + HIDDEN | |
| print(f" Total params: ~{total_params/1e6:.1f}M (training on {len(X_data)} samples)", flush=True) | |
| # Loss function | |
| def mse_loss(pred, target): | |
| return mx.mean((pred - target) ** 2) | |
| # Optimizer | |
| optimizer = optim.Adam(learning_rate=1e-3) | |
| # Training loop | |
| def loss_fn(model, x, y): | |
| pred = model(x) | |
| return mse_loss(pred, y) | |
| n_epochs = 100 | |
| batch_size = min(16, len(X)) | |
| n_batches = max(1, len(X) // batch_size) | |
| print(f"\n🏋️ Training: {n_epochs} epochs, {n_batches} batches/epoch", flush=True) | |
| print(f"{'Epoch':<8} {'Loss':<12} {'NCI(pred,real)':<16} {'Time':<8}", flush=True) | |
| print("-" * 45, flush=True) | |
| for epoch in range(n_epochs): | |
| t0 = time.time() | |
| epoch_loss = 0.0 | |
| # Shuffle | |
| perm = list(range(len(X))) | |
| import random | |
| random.shuffle(perm) | |
| for b in range(n_batches): | |
| idx = perm[b * batch_size : (b + 1) * batch_size] | |
| bx = mx.array([X[i] for i in idx]) | |
| by = mx.array([Y[i] for i in idx]) | |
| # MLX train step | |
| def loss_fn(m): | |
| return mse_loss(m(bx), by) | |
| loss, grads = mx.value_and_grad(loss_fn)(predictor) | |
| optimizer.update(predictor, grads) | |
| epoch_loss += loss.item() | |
| epoch_loss /= n_batches | |
| elapsed = time.time() - t0 | |
| # Evaluate: compute NCI between predicted L48 and real L48 | |
| if epoch % 10 == 0 or epoch == n_epochs - 1: | |
| pred_all = predictor(X) | |
| ncis = [] | |
| for j in range(min(5, len(pred_all))): | |
| nci = mx.sum(pred_all[j] * Y[j]) / (mx.linalg.norm(pred_all[j]) * mx.linalg.norm(Y[j])) | |
| ncis.append(float(nci)) | |
| avg_nci = sum(ncis) / len(ncis) | |
| print(f"{epoch:<8} {epoch_loss:<12.6f} {avg_nci:<16.4f} {elapsed:<8.2f}s", flush=True) | |
| # Final evaluation | |
| print(f"\n📊 Final Evaluation:", flush=True) | |
| pred_all = predictor(X) | |
| ncis = [] | |
| norms_real = [] | |
| norms_pred = [] | |
| for j in range(len(X)): | |
| nci = mx.sum(pred_all[j] * Y[j]) / (mx.linalg.norm(pred_all[j]) * mx.linalg.norm(Y[j])) | |
| ncis.append(float(nci)) | |
| norms_real.append(float(mx.linalg.norm(Y[j]))) | |
| norms_pred.append(float(mx.linalg.norm(pred_all[j]))) | |
| print(f" Mean NCI(predicted, real): {sum(ncis)/len(ncis):.4f}", flush=True) | |
| print(f" Max NCI(predicted, real): {max(ncis):.4f}", flush=True) | |
| print(f" Min NCI(predicted, real): {min(ncis):.4f}", flush=True) | |
| print(f" Mean norm(real L48): {sum(norms_real)/len(norms_real):.1f}", flush=True) | |
| print(f" Mean norm(pred L48): {sum(norms_pred)/len(norms_pred):.1f}", flush=True) | |
| # Save predictor weights | |
| predictor.save_weights("/Users/ai/noema_predictor.safetensors") | |
| print(f"\n💾 Saved predictor to: /Users/ai/noema_predictor.safetensors", flush=True) | |
| print(f"\n{'='*60}") | |
| print(f"✅ DONE — Early Exit Ready!") | |
| print(f" Layers saved: 41/64 = 64%") | |
| print(f" Speedup: ~2.8x") | |
| print(f" Overhead: MLP ~2ms") | |
| print(f"{'='*60}", flush=True) | |