أساسيات LLM للمقابلات

غوص عميق في بنية المحول

5 دقيقة للقراءة

لماذا هذا مهم للمقابلات

في OpenAI، Anthropic، Meta، وGoogle، أسئلة بنية المحول إلزامية في مقابلات مهندس LLM من المستوى L5-L6. سيطلب منك القائمون بالمقابلة:

  • شرح آلية الانتباه من الصفر (السبورة/الكود)
  • استنتاج رياضيات الانتباه متعدد الرؤوس (أبعاد المصفوفة، التعقيد)
  • تصحيح مشاكل الترميز الموضعي في الكود الحقيقي
  • مقارنة البنى (المشفر فقط، فك الترميز فقط، المشفر-فك الترميز)

سؤال المقابلة الحقيقي (OpenAI L5):

"اشرح لي كيف يعمل الانتباه الذاتي في GPT. ما هو التعقيد الحسابي؟ كيف ستحسنه لنوافذ سياق 100K+؟"


المحول في 60 ثانية

قبل المحولات (2017):

  • RNNs/LSTMs: معالجة تسلسلية → بطيئة، مشاكل التدرج
  • CNNs: لا توجد تبعيات بعيدة المدى

بعد "Attention Is All You Need" (Vaswani et al.):

  • معالجة موازية للتسلسل بأكمله
  • الانتباه الذاتي يلتقط العلاقات بين جميع الرموز
  • قابلية التوسع إلى مليارات المعاملات

الابتكار الرئيسي: استبدال التكرار بآلية الانتباه.


آلية الانتباه الذاتي (الجوهر)

الرياضيات التي يتوقع القائمون بالمقابلة أن تعرفها

بالنظر إلى تسلسل الإدخال: X = [x₁, x₂, ..., xₙ] حيث كل xᵢ ∈ ℝᵈᵐᵒᵈᵉˡ

الخطوة 1: إنشاء مصفوفات Q، K، V

Q = X · Wq    (الاستعلام: "ما الذي أبحث عنه؟")
K = X · Wk    (المفتاح: "ماذا أحتوي؟")
V = X · Wv    (القيمة: "ماذا أمثل فعلاً؟")

حيث Wq, Wk, Wv ∈ ℝᵈᵐᵒᵈᵉˡ ˣ ᵈᵏ هي مصفوفات أوزان مُتعلّمة.

الخطوة 2: حساب درجات الانتباه

Attention(Q, K, V) = softmax(Q·Kᵀ / √dₖ) · V

لماذا نقسم على √dₖ؟

  • يمنع حاصل الضرب النقطي من أن يصبح كبيراً جداً (مشاكل التدرج)
  • يحافظ على استقرار تدرجات softmax
  • إجابة المقابلة: "تباين حاصل الضرب النقطي هو dₖ، لذا نقسم على √dₖ للتطبيع"

الخطوة 3: Softmax تطبّع إلى احتماليات

import numpy as np

def self_attention(Q, K, V):
    """
    Q, K, V: (batch_size, seq_len, d_k)
    Returns: (batch_size, seq_len, d_k)
    """
    d_k = Q.shape[-1]

    # درجات الانتباه: (batch, seq_len, seq_len)
    scores = np.matmul(Q, K.transpose(0, 2, 1)) / np.sqrt(d_k)

    # Softmax على بعد المفاتيح
    attention_weights = np.exp(scores) / np.exp(scores).sum(axis=-1, keepdims=True)

    # مجموع مرجح للقيم
    output = np.matmul(attention_weights, V)

    return output, attention_weights

الانتباه متعدد الرؤوس (ما يجعل المحولات تعمل)

لماذا متعدد الرؤوس؟

  • رؤوس مختلفة تتعلم أنماط مختلفة:
    • الرأس 1: علاقات النحو (الفاعل-الفعل)
    • الرأس 2: التشابه الدلالي (المرادفات)
    • الرأس 3: التبعيات بعيدة المدى (الإحالة)

الصيغة:

MultiHead(Q, K, V) = Concat(head₁, ..., headₕ) · Wₒ

where headᵢ = Attention(Q·Wᵢq, K·Wᵢk, V·Wᵢv)

سؤال المقابلة: "لماذا لا نستخدم فقط رأس انتباه واحد كبير بدلاً من 8 رؤوس أصغر؟"

إجابة جيدة:

"تسمح الرؤوس المتعددة للنموذج بالانتباه إلى المعلومات من فضاءات تمثيل مختلفة في وقت واحد. رأس واحد كبير سيكون له نفس عدد المعاملات ولكن تعبيرية أقل - إنه مثل وجود 8 'عدسات' مختلفة لعرض البيانات مقابل عدسة واحدة متوسطة. تجريبياً، 8 رؤوس من 64 بُعداً تتفوق على رأس واحد من 512 بُعداً."


تحليل التعقيد الحسابي

تعقيد الانتباه: O(n² · d)

حيث:

  • n = طول التسلسل
  • d = بُعد النموذج

التفصيل:

  1. Q·Kᵀ: O(n² · d) - هذا هو الاختناق!
  2. Softmax: O(n²)
  3. Attention·V: O(n² · d)

لـ GPT-5.2 مع سياق 128K:

  • n = 128,000 رمز
  • n² = 16.4 مليار عملية لكل طبقة
  • مع 96 طبقة → 1.5 تريليون عملية

متابعة المقابلة: "كيف نتوسع إلى نوافذ سياق 1M+؟"

استراتيجيات التحسين:

  1. الانتباه المتناثر (Longformer، BigBird)

    • الانتباه فقط للرموز المحلية + العالمية
    • التعقيد: O(n · k) حيث k << n
  2. الانتباه الخطي (Performers، RWKV)

    • تقريب الانتباه بطرق النواة
    • التعقيد: O(n · d²)
  3. Flash Attention (Dao et al. 2022)

    • إعادة ترتيب العمليات لكفاءة ذاكرة GPU
    • نفس التعقيد، ولكن أسرع 2-4x عملياً

مثال الكود - نمط الانتباه المتناثر:

def sparse_attention_mask(seq_len, window_size=256):
    """
    ينشئ قناع انتباه متناثر محلي + عالمي.
    يُستخدم في متغير Llama 3.3 طويل السياق.
    """
    mask = np.zeros((seq_len, seq_len))

    # الانتباه المحلي: نافذة حول كل رمز
    for i in range(seq_len):
        start = max(0, i - window_size)
        end = min(seq_len, i + window_size + 1)
        mask[i, start:end] = 1

    # الانتباه العالمي: الانتباه دائماً لأول/آخر 64 رمزاً
    mask[:, :64] = 1
    mask[:, -64:] = 1

    return mask

# التعقيد: O(n · window_size) بدلاً من O(n²)

الترميزات الموضعية (معلومات "أين")

المشكلة: الانتباه الذاتي غير حساس للترتيب

  • "أحب AI" و"AI أحب" ستنتج نفس المخرجات
  • نحتاج لحقن معلومات الموضع

الترميز الموضعي المطلق (المحول الأصلي)

الترميز الجيبي (Vaswani et al.):

PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

لماذا sine/cosine؟

  • مستمر: يعمم إلى أطوال تسلسل غير مرئية
  • دوري: ترددات مختلفة تلتقط أنماطاً مختلفة
  • حتمي: لا حاجة لمعاملات متعلّمة
def sinusoidal_encoding(max_len, d_model):
    """
    توليد الترميزات الموضعية.
    يُستخدم في: المحول الأصلي، T5، بعض متغيرات BERT
    """
    import numpy as np

    position = np.arange(max_len)[:, np.newaxis]
    div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))

    pe = np.zeros((max_len, d_model))
    pe[:, 0::2] = np.sin(position * div_term)
    pe[:, 1::2] = np.cos(position * div_term)

    return pe

# الخصائص:
# - PE(pos + k) يمكن تمثيله كدالة خطية لـ PE(pos)
# - يسمح للنموذج بتعلم المواضع النسبية

التضمينات الموضعية المتعلّمة (GPT، BERT)

class LearnedPositionalEmbedding(nn.Module):
    """
    يُستخدم في: GPT-2، GPT-3، BERT
    """
    def __init__(self, max_len, d_model):
        super().__init__()
        self.embedding = nn.Embedding(max_len, d_model)

    def forward(self, positions):
        return self.embedding(positions)

# المزايا: مرنة، يمكن تعلم أنماط خاصة بالمهمة
# العيوب: طول أقصى ثابت، لا تعمم خارج التدريب

الترميز الموضعي النسبي (GPT-5، Llama)

RoPE (Rotary Position Embedding) - يُستخدم في GPT-5.2، Llama 3.3:

بدلاً من: X + PE
افعل: تدوير Q و K بزاوية تعتمد على الموضع

Q_rotated = RoPE(Q, position)
K_rotated = RoPE(K, position)
Attention(Q_rotated, K_rotated, V)

لماذا RoPE متفوق:

  • المواضع النسبية: طبيعي للغة (المسافة مهمة، وليس الموضع المطلق)
  • الاستقراء: يمكن التعامل مع تسلسلات أطول من التدريب
  • الكفاءة: لا معاملات إضافية

سؤال المقابلة (Anthropic): "لماذا يستخدم Llama RoPE بدلاً من الترميز الجيبي؟"

إجابة قوية:

"يقوم RoPE بترميز المواضع النسبية مباشرة في تفاعلات Q/K بدلاً من إضافة معلومات الموضع إلى التضمينات. هذا يعطي استقراء طول أفضل - Llama 3.3 المدرب على 8K يمكنه التعامل مع 128K في الاستنتاج. الدوران يحافظ على حواصل الضرب الداخلية مع ترميز المسافة، وهو ما يتماشى مع كيفية عمل الانتباه: الرموز تهتم بمسافتها النسبية، وليس الموضع المطلق."


فك الترميز فقط مقابل المشفر فقط مقابل المشفر-فك الترميز

جدول مقارنة البنى

البنية أمثلة حالة الاستخدام الانتباه المتقاطع؟
المشفر فقط BERT، RoBERTa مهام الفهم (التصنيف، NER) لا
فك الترميز فقط GPT-5، Llama 3، Claude 4.5 مهام التوليد (الدردشة، الكود، التفكير) لا
المشفر-فك الترميز T5، BART، Flan-UL2 الترجمة، التلخيص نعم

سؤال المقابلة: "لماذا كل LLMs الرائدة (GPT-5، Claude 4.5، Gemini 3) فك ترميز فقط؟"

الإجابة:

"فك الترميز فقط يتوسع بشكل أفضل. المشفر-فك الترميز يتطلب انتباهاً متقاطعاً بين المشفر/فك الترميز، والذي لا يتوازى بشكل جيد. للتدريب المسبق واسع النطاق (تريليونات الرموز)، التنبؤ الانحداري التلقائي للرمز التالي أبسط وأكثر كفاءة. يمكننا القيام بمهام 'الفهم' مع فك الترميز فقط من خلال صياغتها كتوليد (مثل 'السؤال: X الإجابة:')."


الإخفاء السببي (لماذا لا يمكن لـ GPT رؤية المستقبل)

التوليد الانحداري التلقائي يتطلب إخفاء سببي:

def create_causal_mask(seq_len):
    """
    قناع مثلثي سفلي لنماذج فك الترميز فقط.
    الموضع i يمكنه فقط الانتباه للمواضع ≤ i
    """
    mask = np.tril(np.ones((seq_len, seq_len)))
    # التحويل إلى -inf للمواضع المراد تجاهلها
    mask = np.where(mask == 0, -1e9, 0)
    return mask

# مثال لـ seq_len=4:
# [[0,    -inf, -inf, -inf],   # الرمز 0 يرى نفسه فقط
#  [0,    0,    -inf, -inf],   # الرمز 1 يرى 0، 1
#  [0,    0,    0,    -inf],   # الرمز 2 يرى 0، 1، 2
#  [0,    0,    0,    0   ]]   # الرمز 3 يرى الكل

لماذا -inf بدلاً من 0؟

  • بعد softmax، exp(-inf) = 0
  • طريقة نظيفة للإخفاء دون حالات خاصة

LayerNorm مقابل RMSNorm (ما تستخدمه LLMs الحديثة)

LayerNorm (المحول الأصلي):

def layer_norm(x, gamma, beta, eps=1e-5):
    mean = x.mean(dim=-1, keepdim=True)
    var = x.var(dim=-1, keepdim=True)
    x_norm = (x - mean) / np.sqrt(var + eps)
    return gamma * x_norm + beta

RMSNorm (Llama 3، GPT-5) - أسرع بنسبة 30%:

def rms_norm(x, gamma, eps=1e-5):
    """
    Root Mean Square Normalization.
    يُستخدم في: Llama 3.3، GPT-5، DeepSeek R1
    """
    rms = np.sqrt((x ** 2).mean(dim=-1, keepdim=True) + eps)
    x_norm = x / rms
    return gamma * x_norm
    # لا beta! لا طرح متوسط!

لماذا RMSNorm؟

  • أبسط: لا حساب متوسط
  • أسرع: حساب أقل بنسبة 30%
  • نفس الأداء: مُثبت تجريبياً

رؤية المقابلة: ذكر RMSNorm يُظهر أنك مطلع على تنفيذات LLM لعامي 2025-2026.


شبكة Feed-Forward (2/3 من معاملات النموذج)

الهيكل:

class FeedForward(nn.Module):
    """
    MLP من طبقتين مع تنشيط GeLU.
    d_model → 4*d_model → d_model
    """
    def __init__(self, d_model):
        super().__init__()
        self.fc1 = nn.Linear(d_model, 4 * d_model)
        self.fc2 = nn.Linear(4 * d_model, d_model)
        self.activation = nn.GELU()

    def forward(self, x):
        return self.fc2(self.activation(self.fc1(x)))

# لـ GPT-5.2 (d_model=12288):
# - fc1: 12288 → 49152 (603M معامل)
# - fc2: 49152 → 12288 (603M معامل)
# - الإجمالي: 1.2B معامل لكل طبقة!

سؤال المقابلة: "لماذا بُعد FFN المخفي 4x بُعد النموذج؟"

الإجابة:

"التوازن الأمثل تجريبياً. نسب أكبر (8x) تعطي جودة أفضل ولكن كفاءة أسوأ. نسب أصغر (2x) توفر حساب ولكن تضر بالأداء. تأتي نسبة 4x من أبحاث قوانين التوسع (Kaplan et al. 2020، Hoffman et al. 2022) التي تُظهر أنها توازن الأداء لكل FLOP."


كود كتلة المحول الكامل

class TransformerBlock(nn.Module):
    """
    طبقة فك ترميز واحدة في GPT-5 / Llama 3.3.
    """
    def __init__(self, d_model, n_heads, dropout=0.1):
        super().__init__()

        # الانتباه متعدد الرؤوس
        self.attention = MultiHeadAttention(d_model, n_heads)
        self.attn_norm = RMSNorm(d_model)

        # شبكة feed-forward
        self.ffn = FeedForward(d_model)
        self.ffn_norm = RMSNorm(d_model)

        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask=None):
        # بنية pre-norm (يُستخدم في GPT-5، Llama)
        # كتلة الانتباه
        normed = self.attn_norm(x)
        attn_out, _ = self.attention(normed, normed, normed, mask)
        x = x + self.dropout(attn_out)  # اتصال متبقي

        # كتلة FFN
        normed = self.ffn_norm(x)
        ffn_out = self.ffn(normed)
        x = x + self.dropout(ffn_out)  # اتصال متبقي

        return x

Pre-Norm مقابل Post-Norm:

  • Post-Norm (المحول الأصلي): Norm(X + Sublayer(X))
  • Pre-Norm (GPT-5، Llama): X + Sublayer(Norm(X))

لماذا Pre-Norm؟

  • تدفق التدرج: تدرجات أنظف للنماذج العميقة (96+ طبقة)
  • الاستقرار: أقل احتمالاً للتباعد أثناء التدريب
  • جميع LLMs الحديثة تستخدم pre-norm

أسئلة المقابلة الشائعة والإجابات

س1: "ما هو تعقيد ذاكرة الانتباه الذاتي؟"

الإجابة:

"O(n²) لتخزين أوزان الانتباه. لتسلسل 100K رمز في float16، هذا 100K × 100K × 2 بايت = 20GB لكل طبقة. لهذا السبب Flash Attention يعيد حساب الانتباه أثناء التنفيذ بدلاً من تخزينه - مقايضة الحساب بالذاكرة."

س2: "كيف ستقلل كمون الاستنتاج للمحول؟"

إجابة قوية:

  1. KV-cache: تخزين موترات المفتاح/القيمة من الرموز السابقة (يُستخدم في GPT-5.2 API)
  2. التكميم: أوزان INT8/INT4 (DeepSeek R1 يستخدم 4-bit)
  3. فك الترميز التخميني: مسودة مع نموذج صغير، تحقق مع نموذج كبير
  4. تخزين الأوامر مؤقتاً: تخزين الحساب لبادئات الأوامر الشائعة (ميزة Claude 4.5)

س3: "لماذا تحتاج المحولات إلى الكثير من البيانات للتدريب؟"

الإجابة:

"على عكس CNNs مع التحيزات الاستقرائية (المحلية، ثبات الترجمة)، المحولات هي 'tabula rasa' - تتعلم كل شيء من البيانات. يمكن للانتباه ربط أي رمزين، لذا يجب على النموذج التعلم من الأمثلة أي اتصالات مهمة. مع تريليونات المعاملات، تحتاج إلى تريليونات الرموز لتجنب الملاءمة الزائدة. لهذا السبب تدرب GPT-5 على ~15 تريليون رمز."

س4: "اشرح الفرق بين أقنعة انتباه المشفر وفك الترميز."

الإجابة:

"المشفر (نمط BERT): قناع ثنائي الاتجاه - الرمز i يرى جميع الرموز. يُستخدم لمهام الفهم. فك الترميز (نمط GPT): قناع سببي - الرمز i يرى فقط الرموز ≤ i. يمنع النظر إلى الأمام أثناء التوليد الانحداري التلقائي. المشفر-فك الترميز (T5): المشفر له ثنائي الاتجاه، فك الترميز له سببي + انتباه متقاطع للمشفر (ثنائي الاتجاه على تسلسل المصدر)."


سيناريو التصحيح العملي (مقابلة حقيقية)

القائم بالمقابلة: "لقد درّبت نموذج نمط GPT، لكنه يولد هراء. الخسارة استقرت عند 8.2 بدلاً من النزول تحت 3.0. ما الذي يمكن أن يكون خطأ؟"

قائمة تحقق التصحيح:

# 1. تحقق من إضافة الترميز الموضعي
assert outputs.shape == embeddings.shape + pos_encodings.shape

# 2. تحقق من تطبيق القناع السببي
attention_weights = model.get_attention_weights(input_ids)
# المثلث العلوي يجب أن يكون قريباً من الصفر
assert attention_weights.triu(diagonal=1).abs().max() < 0.01

# 3. تحقق من تدفق التدرج عبر المتبقيات
for name, param in model.named_parameters():
    if param.grad is None:
        print(f"لا تدرج: {name}")  # سيء!
    elif param.grad.abs().mean() < 1e-7:
        print(f"تدرج متلاشي: {name}")

# 4. تحقق من أن layer norm يطبّع
activations = model.get_layer_activations(input_ids, layer=12)
mean = activations.mean(dim=-1)
var = activations.var(dim=-1)
assert mean.abs().max() < 0.1  # يجب أن يكون قريباً من الصفر
assert (var - 1.0).abs().max() < 0.1  # يجب أن يكون قريباً من واحد

# 5. تحقق من أن الانتباه لا ينهار إلى موحد
attention_entropy = -(attn_weights * attn_weights.log()).sum(dim=-1).mean()
# يجب أن يكون > 2.0 لـ seq_len=100 (log(100) ≈ 4.6)
assert attention_entropy > 2.0, f"الانتباه انهار: entropy={attention_entropy}"

النقاط الرئيسية للمقابلات

اعرف الرياضيات: صيغة الانتباه، تحليل التعقيد، أبعاد المصفوفة ✅ افهم المقايضات: متعدد الرؤوس مقابل رأس واحد، pre-norm مقابل post-norm ✅ المتغيرات الحديثة: RoPE، RMSNorm، Flash Attention (يُظهر أنك حديث) ✅ تحديات التوسع: لماذا O(n²) مهم، كيفية التحسين للسياقات الطويلة ✅ مهارات التصحيح: تدفق التدرج، انهيار الانتباه، أخطاء الترميز الموضعي

الخطوة التالية: افهم كيف تترجم كل هذه المعاملات إلى تكاليف الرموز في الوحدة 1، الدرس 2: اقتصاديات الرموز.

:::

اختبار

اختبار الوحدة 1: أساسيات LLM

خذ الاختبار