Ruqiya commited on
Commit
4babff6
·
verified ·
1 Parent(s): 7636920

English comments: the published copy is read internationally

Browse files
Files changed (1) hide show
  1. modeling_ruqlm.py +33 -29
modeling_ruqlm.py CHANGED
@@ -1,16 +1,18 @@
1
  """
2
- معمارية Ruq-LMمحوّل صغير مُدرَّب من الصفر.
3
-
4
- هذا هو النموذج نفسه: أوزان مهيّأة عشوائياً، لا اشتقاق من أي نموذج جاهز.
5
- الوصفة حديثة وقياسية: pre-norm + RMSNorm + RoPE + SwiGLU + تضمينات مربوطة.
6
-
7
- لماذا هذه الخيارات عند 30M بارامتر تحديداً:
8
- - RMSNorm بدل LayerNorm: أقل عمليات، ولا فرق يُذكر في الجودة.
9
- - RoPE بدل تضمينات موضعية مُتعلَّمة: لا بارامترات إضافية، وتعميم أفضل
10
- على أطوال لم تُرَ أثناء التدريب.
11
- - SwiGLU: أفضل من ReLU/GELU عند ثبات عدد البارامترات.
12
- - ربط تضمينات الدخل بالخرج: يوفّر 4.2M بارامتر أي 14% من النموذج
13
- عند مفردات 8192. عند هذا الحجم الصغير هذا فرق جوهري لا تحسين هامشي.
 
 
14
  """
15
 
16
  from __future__ import annotations
@@ -29,8 +31,8 @@ class ModelArgs:
29
  d_model: int = 512
30
  n_layers: int = 8
31
  n_heads: int = 8
32
- n_kv_heads: int | None = None # None = انتباه متعدد الرؤوس عادي؛ أقل = GQA
33
- ffn_hidden: int | None = None # None = يُحسب تلقائياً (~8/3 × d مقرّباً لمضاعف 64)
34
  max_seq_len: int = 512
35
  rope_theta: float = 10000.0
36
  norm_eps: float = 1e-5
@@ -39,14 +41,14 @@ class ModelArgs:
39
 
40
  def __post_init__(self) -> None:
41
  if self.d_model % self.n_heads:
42
- raise ValueError("d_model يجب أن يقبل القسمة على n_heads")
43
  if self.n_kv_heads is None:
44
  self.n_kv_heads = self.n_heads
45
  if self.n_heads % self.n_kv_heads:
46
- raise ValueError("n_heads يجب أن يقبل القسمة على n_kv_heads")
47
  if self.ffn_hidden is None:
48
- # 8/3 × d بدل 4 × d: SwiGLU يستخدم ثلاث مصفوفات لا اثنتين،
49
- # فنقلّص العرض للحفاظ على نفس ميزانية البارامترات.
50
  self.ffn_hidden = 64 * math.ceil((8 * self.d_model / 3) / 64)
51
 
52
  @property
@@ -57,7 +59,7 @@ class ModelArgs:
57
  return asdict(self)
58
 
59
 
60
- # --------------------------------------------------------------------- الطبقات
61
  class RMSNorm(nn.Module):
62
  def __init__(self, dim: int, eps: float = 1e-5):
63
  super().__init__()
@@ -65,7 +67,8 @@ class RMSNorm(nn.Module):
65
  self.weight = nn.Parameter(torch.ones(dim))
66
 
67
  def forward(self, x: torch.Tensor) -> torch.Tensor:
68
- # يُحسب في float32 دائماً: التطبيع في bf16 يفقد دقة تُهم عند العمق
 
69
  dtype = x.dtype
70
  x = x.float()
71
  x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
@@ -73,7 +76,7 @@ class RMSNorm(nn.Module):
73
 
74
 
75
  def build_rope_cache(seq_len: int, head_dim: int, theta: float, device, dtype):
76
- """يعيد (cos, sin) بشكل (seq_len, head_dim/2)."""
77
  inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim))
78
  pos = torch.arange(seq_len, device=device).float()
79
  freqs = torch.outer(pos, inv_freq)
@@ -81,7 +84,7 @@ def build_rope_cache(seq_len: int, head_dim: int, theta: float, device, dtype):
81
 
82
 
83
  def apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
84
- """x بشكل (B, H, S, D) — يدوّر كل زوج إحداثيات بزاوية تتناسب مع الموضع."""
85
  x1, x2 = x.chunk(2, dim=-1)
86
  cos = cos[None, None, : x.size(-2), :]
87
  sin = sin[None, None, : x.size(-2), :]
@@ -146,7 +149,7 @@ class Block(nn.Module):
146
  return x + self.drop(self.ffn(self.ffn_norm(x)))
147
 
148
 
149
- # --------------------------------------------------------------------- النموذج
150
  class RuqLM(nn.Module):
151
  def __init__(self, args: ModelArgs):
152
  super().__init__()
@@ -161,8 +164,9 @@ class RuqLM(nn.Module):
161
  self.lm_head.weight = self.tok_emb.weight
162
 
163
  self.apply(self._init)
164
- # تدرّج المسارات المتبقية ينمو مع العمق؛ نقلّص أوزان الإسقاط الأخير
165
- # في كل كتلة بـ 1/sqrt(2L) للحفاظ على تباين ثابت عبر الطبقات (GPT-2).
 
166
  std = 0.02 / math.sqrt(2 * args.n_layers)
167
  for block in self.blocks:
168
  nn.init.normal_(block.attn.wo.weight, mean=0.0, std=std)
@@ -198,7 +202,7 @@ class RuqLM(nn.Module):
198
 
199
  loss = None
200
  if labels is not None:
201
- # الإزاحة: الموضع i يتنبأ بالتوكن i+1
202
  loss = F.cross_entropy(
203
  logits[:, :-1].reshape(-1, logits.size(-1)).float(),
204
  labels[:, 1:].reshape(-1),
@@ -206,9 +210,9 @@ class RuqLM(nn.Module):
206
  )
207
  return logits, loss
208
 
209
- # ------------------------------------------------------------- الإحصاءات
210
  def num_params(self, embeddings: bool = True) -> int:
211
- """التضمينات المربوطة تُحسب مرة واحدة (lm_head.weight هو نفسه tok_emb.weight)."""
212
  seen, total = set(), 0
213
  for name, p in self.named_parameters():
214
  if id(p) in seen:
@@ -222,7 +226,7 @@ class RuqLM(nn.Module):
222
  @torch.no_grad()
223
  def generate(self, input_ids, max_new_tokens=128, temperature=0.8,
224
  top_k=50, eos_id=None):
225
- """توليد بسيط بلا كاش KV — كافٍ للتقييم على تسلسلات قصيرة."""
226
  self.eval()
227
  for _ in range(max_new_tokens):
228
  window = input_ids[:, -self.args.max_seq_len:]
 
1
  """
2
+ RuqLM architecturea small transformer trained from scratch.
3
+
4
+ This is the model itself: randomly initialised weights, not derived from any
5
+ pretrained checkpoint. The recipe is modern and standard: pre-norm, RMSNorm,
6
+ RoPE, SwiGLU, tied embeddings.
7
+
8
+ Why these choices at 30M parameters specifically:
9
+ - RMSNorm over LayerNorm: fewer operations, no measurable quality cost.
10
+ - RoPE over learned positional embeddings: no extra parameters, and better
11
+ generalisation to lengths not seen during training.
12
+ - SwiGLU: better than ReLU/GELU at a fixed parameter count.
13
+ - Tying input and output embeddings: saves 4.2M parameters, 14% of the model
14
+ at a vocabulary of 8192. At this size that is structural, not a marginal
15
+ optimisation.
16
  """
17
 
18
  from __future__ import annotations
 
31
  d_model: int = 512
32
  n_layers: int = 8
33
  n_heads: int = 8
34
+ n_kv_heads: int | None = None # None = plain multi-head attention; fewer = GQA
35
+ ffn_hidden: int | None = None # None = derived (~8/3 x d, rounded to 64)
36
  max_seq_len: int = 512
37
  rope_theta: float = 10000.0
38
  norm_eps: float = 1e-5
 
41
 
42
  def __post_init__(self) -> None:
43
  if self.d_model % self.n_heads:
44
+ raise ValueError("d_model must be divisible by n_heads")
45
  if self.n_kv_heads is None:
46
  self.n_kv_heads = self.n_heads
47
  if self.n_heads % self.n_kv_heads:
48
+ raise ValueError("n_heads must be divisible by n_kv_heads")
49
  if self.ffn_hidden is None:
50
+ # 8/3 x d rather than 4 x d: SwiGLU uses three matrices instead of
51
+ # two, so the width shrinks to hold the parameter budget constant.
52
  self.ffn_hidden = 64 * math.ceil((8 * self.d_model / 3) / 64)
53
 
54
  @property
 
59
  return asdict(self)
60
 
61
 
62
+ # ----------------------------------------------------------------------- layers
63
  class RMSNorm(nn.Module):
64
  def __init__(self, dim: int, eps: float = 1e-5):
65
  super().__init__()
 
67
  self.weight = nn.Parameter(torch.ones(dim))
68
 
69
  def forward(self, x: torch.Tensor) -> torch.Tensor:
70
+ # Always computed in float32: normalising in bf16 loses precision that
71
+ # matters once the network is deep.
72
  dtype = x.dtype
73
  x = x.float()
74
  x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
 
76
 
77
 
78
  def build_rope_cache(seq_len: int, head_dim: int, theta: float, device, dtype):
79
+ """Returns (cos, sin), each of shape (seq_len, head_dim/2)."""
80
  inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim))
81
  pos = torch.arange(seq_len, device=device).float()
82
  freqs = torch.outer(pos, inv_freq)
 
84
 
85
 
86
  def apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
87
+ """x is (B, H, S, D) — rotates each coordinate pair by an angle set by position."""
88
  x1, x2 = x.chunk(2, dim=-1)
89
  cos = cos[None, None, : x.size(-2), :]
90
  sin = sin[None, None, : x.size(-2), :]
 
149
  return x + self.drop(self.ffn(self.ffn_norm(x)))
150
 
151
 
152
+ # ------------------------------------------------------------------------ model
153
  class RuqLM(nn.Module):
154
  def __init__(self, args: ModelArgs):
155
  super().__init__()
 
164
  self.lm_head.weight = self.tok_emb.weight
165
 
166
  self.apply(self._init)
167
+ # Variance on the residual stream grows with depth, so the final
168
+ # projection in each block is scaled down by 1/sqrt(2L) to hold it
169
+ # roughly constant across layers (GPT-2).
170
  std = 0.02 / math.sqrt(2 * args.n_layers)
171
  for block in self.blocks:
172
  nn.init.normal_(block.attn.wo.weight, mean=0.0, std=std)
 
202
 
203
  loss = None
204
  if labels is not None:
205
+ # Shifted: position i predicts token i+1
206
  loss = F.cross_entropy(
207
  logits[:, :-1].reshape(-1, logits.size(-1)).float(),
208
  labels[:, 1:].reshape(-1),
 
210
  )
211
  return logits, loss
212
 
213
+ # -------------------------------------------------------------------- stats
214
  def num_params(self, embeddings: bool = True) -> int:
215
+ """Tied embeddings are counted once (lm_head.weight is tok_emb.weight)."""
216
  seen, total = set(), 0
217
  for name, p in self.named_parameters():
218
  if id(p) in seen:
 
226
  @torch.no_grad()
227
  def generate(self, input_ids, max_new_tokens=128, temperature=0.8,
228
  top_k=50, eos_id=None):
229
+ """Plain sampling without a KV cache adequate for short sequences."""
230
  self.eval()
231
  for _ in range(max_new_tokens):
232
  window = input_ids[:, -self.args.max_seq_len:]