File size: 9,943 Bytes
0e0b62a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
"""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)