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 | صرفهجویی |
|---|---|---|---|---|---|---|---|
| gpt2 | LayerNorm | ۱۲ | ۷۶۸ | layer_norm_epsilon | ۳۸٬۴۰۰ | ۱۹٬۲۰۰ | ۱۹٬۲۰۰ |
| pythia-410m | LayerNorm | ۲۴ | ۱۰۲۴ | layer_norm_eps | ۱۰۰٬۳۵۲ | ۵۰٬۱۷۶ | ۵۰٬۱۷۶ |
| phi-2 | LayerNorm | ۳۲ | ۲٬۵۶۰ | layer_norm_eps | ۳۳۲٬۸۰۰ | ۱۶۶٬۴۰۰ | ۱۶۶٬۴۰۰ |
| stablelm-3b-4e1t | LayerNorm | ۳۲ | ۲٬۵۶۰ | layer_norm_eps | ۳۳۲٬۸۰۰ | ۱۶۶٬۴۰۰ | ۱۶۶٬۴۰۰ |
| TinyLlama-1.1B | RMSNorm | ۲۲ | ۲٬۰۴۸ | rms_norm_eps | ۱۸۴٬۳۲۰ | ۹۲٬۱۶۰ | ۹۲٬۱۶۰ |
| Qwen3-0.6B | RMSNorm | ۲۸ | ۱۰۲۴ | rms_norm_eps | ۱۱۶٬۷۳۶ | ۵۸٬۳۶۸ | ۵۸٬۳۶۸ |
| SmolLM2-360M | RMSNorm | ۳۲ | ۹۶۰ | rms_norm_eps | ۱۲۴٬۸۰۰ | ۶۲٬۴۰۰ | ۶۲٬۴۰۰ |
| OLMo-2-0425-1B | RMSNorm | ۱۶ | ۲٬۰۴۸ | rms_norm_eps | ۱۳۵٬۱۶۸ | ۶۷٬۵۸۴ | ۶۷٬۵۸۴ |
| Mistral-7B-v0.1 | RMSNorm | ۳۲ | ۴٬۰۹۶ | 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 میاندازد.
منابع
- RMSNorm — arXiv:1910.07467، ۱۶ اکتبر ۲۰۱۹
- Layer Normalization — arXiv:1607.06450، ۱۶ ژوئیه ۲۰۱۶
- کد Qwen3RMSNorm در transformers
- LLaMA — arXiv:2302.13971، ۲۷ فوریه ۲۰۲۳
- کد nn.LayerNorm در GPT-2
- کد LlamaRMSNorm در LLaMA
- مستندات torch.nn.RMSNorm
- مستندات torch.nn.LayerNorm
- پیادهسازی مرجع RMSNorm
- config.json مدل Qwen3-0.6B
- config.json مدل Mistral-7B-v0.1
- شمار پارامترهای Mistral-7B-v0.1 در API
دیدگاهها
۰ موردهنوز دیدگاهی ثبت نشده. اولین نفر باشید.