RMSNorm نرمال‌سازی استاندارد در مدل‌های تازه است و یک کلمه از LayerNorm کم دارد: کم کردن میانگین، و بایاسی که فقط برای همان کار لازم بود. روی برداری که خودمان ساختیم، میانگین یک توکن ۰٫۲۶۸۹۵۰ بود؛ LayerNorm آن را به صفر رساند و RMSNorm همان ۰٫۰۸۷۰۶۱ را نگه داشت. در این نوشته هر دو را با پایتون خالص کنار هم می‌گذاریم، بعد config.json نه مدل واقعی را می‌خوانیم تا ببینیم این حذف در هر مدل چند پارامتر کنار می‌گذارد. لحظه‌ی خواندن: ۲۸ سپتامبر ۲۰۲۶.

دو فرمول، یک تفاوت: میانگین یا ریشه‌ی میانگین مربعات

LayerNorm روی هر توکن یک میانگین می‌گیرد، یک انحراف معیار می‌گیرد و بردار را حول همان میانگین می‌چیند، و در پایان ضرب در دو بردار پارامتر به‌علاوه‌ی بایاس می‌کند. فرمول کاملش در مقاله‌ی Ba و همکاران آمده است.

RMSNorm همان کار را با یک تفاوت انجام می‌دهد: نه میانگین را کم می‌کند و نه بایاس دارد. مقاله‌ی اصلی RMSNorm در ۱۶ اکتبر ۲۰۱۹ منتشر شد و فرض بنیادی‌اش را صریح نوشت: ناوردایی نسبت به جابه‌جایی میانگین در LayerNorm لازم نیست. همان مقاله گزارش می‌کند RMSNorm روی مدل‌های مختلف زمان اجرا را بین ۷ تا ۶۴ درصد کاهش داده، بدون افت کیفیت نسبت به LayerNorm.

کد زیر هر دو را روی یک ورودی یکسان اجرا می‌کند و هیچ بسته‌ی بیرونی نمی‌خواهد.

# این فایل را به نام norm_demo.py در یک پوشه‌ی خالی ذخیره کنید، و اجرای آن:
$ python3 norm_demo.py

import random
import time

random.seed(7)
D = 1024          # hidden_size در Qwen3-0.6B
B, T = 8, 64      # batch و تعداد توکن
EPS = 1e-6        # مقدار rms_norm_eps در همان config.json


def layer_norm(v, w, b, eps=EPS):
    mu = sum(v) / len(v)
    var = sum((t - mu) ** 2 for t in v) / len(v)
    inv = 1.0 / (var + eps) ** 0.5
    return [(t - mu) * inv * wi + bi for t, wi, bi in zip(v, w, b)]


def rms_norm(v, w, eps=EPS):
    ms = sum(t * t for t in v) / len(v)
    inv = 1.0 / (ms + eps) ** 0.5
    return [t * inv * wi for t, wi in zip(v, w)]


# فعالیت با انحراف معیار ۳، مثل چیزی که در یک رمزگذار واقعی دیده می‌شود
x = [[random.gauss(0.0, 3.0) for _ in range(D)] for _ in range(B * T)]
w = [1.0] * D
b = [0.0] * D

t0 = time.perf_counter()
ln = [layer_norm(v, w, b) for v in x]
t_ln = time.perf_counter() - t0

t0 = time.perf_counter()
rn = [rms_norm(v, w) for v in x]
t_rms = time.perf_counter() - t0

print(f"token={B * T}  hidden={D}")
print(f"input mean/token  : {max(abs(sum(v) / D) for v in x):.6f}")
print(f"LayerNorm mean    : {max(abs(sum(v) / D) for v in ln):.2e}")
print(f"RMSNorm mean      : {max(abs(sum(v) / D) for v in rn):.6f}")
print(f"ms: LN={t_ln * 1e3:.1f}  RMS={t_rms * 1e3:.1f}")
print(f"ratio LN/RMS     = {t_ln / t_rms:.2f}x")

این هم خروجی واقعی همان اجرا روی همین سرور است.

token=512  hidden=1024
input mean/token  : 0.268950
LayerNorm mean    : 3.75e-17
RMSNorm mean      : 0.087061
ms: LN=90.9  RMS=55.7
ratio LN/RMS     = 1.63x

سطر چهارم و پنجم مهم‌ترین سطرهای این اجرا هستند. میانگین ورودی یک توکن تا ۰٫۲۶۸۹۵۰ بزرگ بود، LayerNorm آن را به ۳٫۷۵ در ۱۰ به توان منفی ۱۷ رساند که در حساب اعشاری همان صفر است، و RMSNorm همان ۰٫۰۸۷۰۶۱ را نگه داشت. این دقیقا همان چیزی است که مقاله‌ی اصلی «ناوردایی نسبت به جابه‌جایی» نامیده است: LayerNorm این اطلاعات را پاک می‌کند و RMSNorm نگه می‌دارد.

سطر آخر زمان را نشان می‌دهد، ولی با احتیاط خوانده شود: این نسبت در پایتون خالص، روی ۵۱۲ توکن و یک هسته اندازه‌گیری شده و روی پردازنده‌ی گرافیکی عوض می‌شود. آنچه روی هر سخت‌افزاری ثابت می‌ماند، تعداد عملیات است: ۲ ضرب برای هر مؤلفه در RMSNorm و ۴ ضرب در LayerNorm.

اگر تازه با معماری ترنسفورمر آشنا شده‌اید، نوشته‌ی ما درباره‌ی سازوکار توجه توضیح می‌دهد چه چیزی پیش از این لایه اتفاق می‌افتد و نوشته‌ی RoPE سراغ موقعیت‌گذاری داخل همان بلوک رفته است. نرمال‌سازی سومین تکه‌ی همان بلوک رمزگذار است.

آنچه config.json مدل‌ها درباره‌ی نرمال‌سازی می‌گوید

برای این بررسی لازم نبود وزن‌های یک مدل واقعی را دانلود کنیم. هر مدل روی Hugging Face یک config.json عمومی دارد که نوع نرمال‌سازی و مقدار eps را در خود نگه می‌دارد. نام کلید eps بین خانواده‌ها یکسان نیست و همین، تله‌ی عملی این بخش است.

مدلنرمال‌سازیلایهبعد ژرفاکلید epsپارامتر LNپارامتر RMSصرفه‌جویی
gpt2LayerNorm۱۲۷۶۸layer_norm_epsilon۳۸٬۴۰۰۱۹٬۲۰۰۱۹٬۲۰۰
pythia-410mLayerNorm۲۴۱۰۲۴layer_norm_eps۱۰۰٬۳۵۲۵۰٬۱۷۶۵۰٬۱۷۶
phi-2LayerNorm۳۲۲٬۵۶۰layer_norm_eps۳۳۲٬۸۰۰۱۶۶٬۴۰۰۱۶۶٬۴۰۰
stablelm-3b-4e1tLayerNorm۳۲۲٬۵۶۰layer_norm_eps۳۳۲٬۸۰۰۱۶۶٬۴۰۰۱۶۶٬۴۰۰
TinyLlama-1.1BRMSNorm۲۲۲٬۰۴۸rms_norm_eps۱۸۴٬۳۲۰۹۲٬۱۶۰۹۲٬۱۶۰
Qwen3-0.6BRMSNorm۲۸۱۰۲۴rms_norm_eps۱۱۶٬۷۳۶۵۸٬۳۶۸۵۸٬۳۶۸
SmolLM2-360MRMSNorm۳۲۹۶۰rms_norm_eps۱۲۴٬۸۰۰۶۲٬۴۰۰۶۲٬۴۰۰
OLMo-2-0425-1BRMSNorm۱۶۲٬۰۴۸rms_norm_eps۱۳۵٬۱۶۸۶۷٬۵۸۴۶۷٬۵۸۴
Mistral-7B-v0.1RMSNorm۳۲۴٬۰۹۶rms_norm_eps۵۳۲٬۴۸۰۲۶۶٬۲۴۰۲۶۶٬۲۴۰

روش حساب ساده است: در یک رمزگذار ۲L+1 نرمال‌سازی هست، پس کافی است با بعد ژرفا ضرب کنید. هر عدد این جدول از فایل خوانده شده، نه از حافظه.

# فایل read_norms.py - نوع نرمال‌سازی را از config.json مدل‌های واقعی بخوان
import json
import urllib.request

UA = {"User-Agent": "Mozilla/5.0 (hoosh)"}
MODELS = [
    "openai-community/gpt2",
    "EleutherAI/pythia-410m",
    "microsoft/phi-2",
    "stabilityai/stablelm-3b-4e1t",
    "TinyLlama/TinyLlama-1.1B-Chat-v1.0",
    "Qwen/Qwen3-0.6B",
    "HuggingFaceTB/SmolLM2-360M",
    "allenai/OLMo-2-0425-1B",
    "mistralai/Mistral-7B-v0.1",
]


def get(url):
    # هدر سفارشی لازم است؛ درخواست بدون آن پاسخ ۴۰۳ می‌گیرد
    r = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(r, timeout=40) as f:
        return f.read().decode()


print(f"{'model':<22}{'kind':<10}{'L':>4}{'hid':>6}{'eps':>8}{'key':<20}{'saved':>8}")
for m in MODELS:
    cfg = json.loads(get(f"https://huggingface.co/{m}/raw/main/config.json"))
    H = cfg.get("hidden_size") or cfg["n_embd"]
    L = cfg.get("num_hidden_layers") or cfg["n_layer"]
    # کلید eps نام ثابتی ندارد؛ خودِ فایل باید درباره‌ی آن دروغ نگوید
    key = next(k for k in ("rms_norm_eps", "layer_norm_eps",
                           "layer_norm_epsilon") if k in cfg)
    n = 2 * L + 1                      # پیش از توجه، پس از توجه، و نرمال نهایی
    print(f"{m.split('/')[-1]:<22}{'RMSNorm' if key == 'rms_norm_eps' else 'LayerNorm':<10}"
          f"{L:>4}{H:>6}{cfg[key]:>8}{key:<20}{n * H:>8}")

و این هم خروجی واقعی همان اجرا، دقیقا با ترتیب جدول بالا.

model                 kind         L   hid     epskey                    saved
gpt2                  LayerNorm   12   768   1e-05layer_norm_epsilon     19200
pythia-410m           LayerNorm   24  1024   1e-05layer_norm_eps        50176
phi-2                 LayerNorm   32  2560   1e-05layer_norm_eps       166400
stablelm-3b-4e1t       LayerNorm   32  2560   1e-05layer_norm_eps       166400
TinyLlama-1.1B-Chat-v1.0RMSNorm     22  2048   1e-05rms_norm_eps           92160
Qwen3-0.6B            RMSNorm     28  1024   1e-06rms_norm_eps           58368
SmolLM2-360M          RMSNorm     32   960   1e-05rms_norm_eps           62400
OLMo-2-0425-1B        RMSNorm     16  2048   1e-06rms_norm_eps           67584
Mistral-7B-v0.1       RMSNorm     32  4096   1e-05rms_norm_eps          266240

ستون پنجم جدول، تله‌ی عملی این بخش است. نام کلید eps بین خانواده‌ها یکی نیست: gpt2 کلید layer_norm_epsilon دارد و phi-2 و pythia کلید layer_norm_eps، و همین یک حرف اختلاف اسکریپتی را که نام کلید را دستی نوشته باشد با خطای KeyError می‌اندازد. مقدار eps هم یکسان نیست و بین 1e-6 و 1e-5 نوسان دارد.

اگر بخواهید بدانید یک مدل در کد واقعی‌اش کدام نرمال‌سازی را می‌سازد، config.json کافی نیست و باید فایل مدل‌سازی را خواند. در مدل‌سازی Qwen3 روی شاخه‌ی main کتابخانه، سطر ۲۹۱ و ۲۹۲ هر لایه را با Qwen3RMSNorm می‌سازد و سطر ۳۵۶ نرمال نهایی را؛ در مدل‌سازی GPT-2 هر لایه دو nn.LayerNorm دارد که هر کدام دو پارامتر دارند. همین تفاوت است که ستون آخر جدول را می‌سازد.

در کد Qwen3 هیچ اثری از بایاس نیست: کلاس Qwen3RMSNorm فقط یک بردار weight با مقدار اولیه‌ی یک می‌سازد و eps را در یک صفت نگه می‌دارد. همان LlamaRMSNorm در مدل‌سازی LLaMA هم دیده می‌شود.

این تفاوت در عمل کجا به شما می‌رسد

عدد ۲۶۶٬۲۴۰ پارامتری که ستون آخر جدول برای Mistral-7B نشان می‌دهد، در برابر کل مدل چیز زیادی نیست: ۲۶۶٬۲۴۰ تقسیم بر ۷٬۲۴۱٬۷۳۲٬۰۹۶ برابر ۰٫۰۰۳۷ درصد. این عدد با ۶۵ نرمال‌سازی در ۳۲ لایه سازگار است.

پس نبایید منتظر کاهش قابل توجه حجم مدل باشید؛ جایی که این تفاوت را می‌بینید، هزینه‌ی حافظه و زمان در هر توکن است. مدل Mistral-7B-v0.1 دقیقا ۷٬۲۴۱٬۷۳۲٬۰۹۶ پارامتر دارد و در دقت sh۱۶ حدود ۱۴٫۴۸ گیگابایت جا می‌گیرد، چون هر پارامتر دو بایت است.

جایی که جایگزینی از سال ۲۰۲۳ سرعت گرفت، خودِ LLaMA بود. مقاله‌ی LLaMA که در ۲۷ فوریه ۲۰۲۳ ثبت شد، از RMSNorm به‌جای LayerNorm استفاده کرد. بعد از آن TinyLlama، Qwen3، SmolLM2 و OLMo-2 همه با RMSNorm آمدند و همین الگو در ستون دوم جدول بالا دیده می‌شود.

اگر کلیدی به نام rms_norm_eps دیدید، یک عملیات کمتر در هر توکن و یک بردار بایاس کمتر در هر لایه‌ی نرمال‌سازی دارید؛ اگر layer_norm_eps دیدید، دقیقا همان چیزی را دارید که GPT-2 دارد.

برای اجرای همین فرمول‌ها روی سخت‌افزار خودتان، PyTorch هر دو نوع را دارد: torch.nn.RMSNorm و torch.nn.LayerNorm. پیاده‌سازی مرجعی که خودِ مقاله معرفی کرده هم در ریپوی rmsnorm است.

جمع‌بندی

RMSNorm، LayerNorm بهینه‌شده نیست. LayerNorm است با یک کلمه برداشته‌شده: کم کردن میانگین، و بایاسی که فقط برای همان کار لازم بود. از نُه مدلی که در جدول بالا خواندیم، پنج مدل RMSNorm و چهار مدل LayerNorm بودند، و هر چهار مدل LayerNorm فقط از دو نام کلید قدیمی‌تر استفاده می‌کنند: layer_norm_eps و layer_norm_epsilon.

قاعده‌ی عملی که از این نوشته بیرون می‌آید این است: نرمال‌سازی را از config.json مدل خودتان بخوانید، نه از یک قاعده‌ی کلی درباره‌ی «مدل‌های تازه». هر مدل این لایه را خودش اعلام می‌کند، و اختلاف یک حرف در نام کلید eps، اسکریپت شما را با KeyError می‌اندازد.

منابع

  1. RMSNorm — arXiv:1910.07467، ۱۶ اکتبر ۲۰۱۹
  2. Layer Normalization — arXiv:1607.06450، ۱۶ ژوئیه ۲۰۱۶
  3. کد Qwen3RMSNorm در transformers
  4. LLaMA — arXiv:2302.13971، ۲۷ فوریه ۲۰۲۳
  5. کد nn.LayerNorm در GPT-2
  6. کد LlamaRMSNorm در LLaMA
  7. مستندات torch.nn.RMSNorm
  8. مستندات torch.nn.LayerNorm
  9. پیاده‌سازی مرجع RMSNorm
  10. config.json مدل Qwen3-0.6B
  11. config.json مدل Mistral-7B-v0.1
  12. شمار پارامترهای Mistral-7B-v0.1 در API