المستوى المطلوب: متوسط — يفترض إنك جربت تشغّل موديل LLM محلياً عبر transformers أو llama.cpp مرة على الأقل، وتعرف إيه يعني FP16 وVRAM.
لو حاولت تشغّل Llama 3.1 70B على RTX 4090 وقالك "CUDA out of memory"، الموديل في صيغته الأصلية محتاج حوالي 140GB ذاكرة، والكارت عندك 24GB. الفرق مش هندسة. الفرق إنك بتنزّل الأوزان بدقة FP16. بـ4-bit Quantization تقدر تنزّله لحوالي 40GB، وتشغّله على A100 80GB واحد بدل 2 إلى 4 منهم، بنفس جودة الإجابات تقريباً وتوفير عشرات الآلاف من الدولارات على الـhardware.
Quantization: ازاي بنضغط الموديل وبيفضل بيشتغل؟
المشكلة باختصار
كل باراميتر في الموديل بيتخزّن كرقم عشري. الافتراضي FP16 يعني 16 بت لكل باراميتر = 2 بايت. Llama 3.1 70B عنده 70 مليار باراميتر، يبقى 70 × 2 = 140GB ذاكرة فقط للأوزان، قبل أي activations أو KV cache.
الكروت اللي بتتحمّل ده قليلة وغالية. H100 80GB حوالي 30 ألف دولار، A100 80GB حوالي 15 ألف، RTX 4090 بـ24GB بس. لو عايز تشغّل 70B بـFP16، محتاج 2× A100 على الأقل، وغالباً 4 لو فيه context كبير. التكلفة بتطلع 60 ألف دولار وفوق. السؤال: هل لازم نخزّن كل وزن بـ16 بت أصلاً؟
المثال البسيط: ضغط الصورة من 16 مليون لون لـ256
تخيّل عندك صورة فوتوغرافية بـ16 مليون لون (RGB كاملة). لو حوّلتها لصيغة palette فيها 256 لون مختار بعناية، حجم الملف بينزل 6 أضعاف، والصورة من بعيد بتفضل تشبه الأصل. السبب إن العين البشرية ما بتلاحظش الفرق بين 256 لون و16 مليون في معظم المشاهد.
الـquantization بنفس الفكرة بالظبط. بدل ما كل وزن يتخزّن بدقة 16 بت (65 ألف قيمة ممكنة)، بنخزّنه في 4 بت (16 قيمة ممكنة) من خلال جدول صغير محفوظ مع الموديل. الفرق إن الموديل بيشتغل تمام بالقيم المضغوطة لأن أوزان الـtransformer أصلاً متوزّعة Normal Distribution ضيّقة حوالي الصفر. مفيش أوزان متطرفة كتير، فالـ16 قيمة كفاية تمثّل التوزيع كله بأقل خطأ.
التعريف العلمي الدقيق: NormalFloat 4
الـNormalFloat 4 (اختصاره NF4) ابتكره Tim Dettmers في ورقة QLoRA المنشورة في NeurIPS 2023. الفكرة العلمية:
- الأوزان في الـtransformer متوزّعة Normal Distribution N(0, σ) بمتوسط ≈ 0.
- بدل ما نقسم المدى للقيم بالتساوي (linear quantization، اللي بيخسر دقة في القيم الصغيرة لأن أغلب الأوزان قريبة من الصفر)، بنختار 16 قيمة information-theoretically optimal تمثّل الـnormal distribution بأقل خطأ تربيعي.
- الـ16 قيمة دي ثابتة محسوبة سلفاً من quantiles توزيع N(0,1) القياسي.
- كل tensor (مش كل وزن) بنحسبله scale واحد بنضربه فيه عشان نرجّع المدى الأصلي. الطريقة دي اسمها block-wise absolute max quantization مع block size = 64.
- الـscales نفسها لو خزّناها بـFP32 بتكلّف ذاكرة. الورقة قدّمت Double Quantization: نضغط الـscales كمان بـFP8، فبنوفّر ~0.4 بت إضافي لكل باراميتر.
النتيجة العلمية: NF4 + double quantization بيطابق أداء BFloat16 على benchmarks اللغة الكبيرة، بينما FP4 (linear quantization البسيط) بيخسر حوالي نقطة كاملة في الـperplexity على نفس الموديل.
الكود الشغّال على Hugging Face
التطبيق فعلياً في 15 سطر بايثون، مفيش حاجة CUDA يدوية ولا compile manual. كل اللي محتاجه: transformers، bitsandbytes، وaccelerate.
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch
# إعداد NF4 مع double quantization و bfloat16 compute
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
model_id = "meta-llama/Meta-Llama-3.1-70B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
)
prompt = "اشرحلي ال speculative decoding في 3 جمل."
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
out = model.generate(**inputs, max_new_tokens=200, do_sample=False)
print(tokenizer.decode(out[0], skip_special_tokens=True))
أربعة باراميترات حاسمة لازم تفهمها قبل ما تنشر الكود ده في إنتاج:
bnb_4bit_quant_type="nf4"— استخدم NF4. الافتراضيfp4وهو أسوأ بحوالي 1% perplexity على نفس الموديل.bnb_4bit_compute_dtype=torch.bfloat16— الأوزان مخزّنة 4 بت لكن الحسابات نفسها بتتم بـbfloat16 بعد dequantization فوري داخل الـkernel. لو حطيت float16 ممكن تلاقي overflow في الموديلز الكبيرة.bnb_4bit_use_double_quant=True— يضغط الـscales، يوفّر ~3.5GB إضافيين على Llama 70B.device_map="auto"— يوزّع الـlayers على الـGPUs المتاحة تلقائياً، ولو محتاج CPU offloading بيحطه على RAM.
الأرقام المقاسة على A100 80GB
على Llama 3.1 70B Instruct، context window 4K، prompt متوسطه 800 توكن، batch size = 1:
- FP16 الأصلي: 140GB أوزان + 12GB activations ≈ 152GB → محتاج 2× A100. سرعة ~38 tok/s.
- NF4 + double quant: ~36GB أوزان + 8GB activations ≈ 44GB → A100 واحد كافي. سرعة ~27 tok/s.
- Perplexity على wikitext-2: FP16 = 4.31، NF4 = 4.36. فرق 1.2%.
- MMLU 5-shot: FP16 = 81.7%، NF4 = 81.2%. فرق 0.5 نقطة.
التوفير المالي: من 2× A100 (~30 ألف دولار) لـA100 واحد (~15 ألف). والسرعة بتنزل ~28% بس. الأرقام دي تقديرية ومتقاربة مع اللي منشور في تقارير bitsandbytes الرسمية وتجارب المجتمع على Hugging Face.
Trade-offs اللي مفيش حد بيقولهالك
- السرعة بتقل لما الـbatch صغير. الـdequantization بياخد ~30% من زمن الـforward pass. لو batch=1 (interactive chat)، الفرق ملحوظ. لو batch=8 أو أكتر، الـbottleneck بيرجع للـmemory bandwidth والفرق بين NF4 وFP16 بيختفي تقريباً.
- Long context بيكسر التوفير. الـKV cache مش متأثر بالـquantization (لازم FP16 افتراضياً). Context 32K على Llama 70B = ~21GB KV cache لوحده. لو محتاج context طويل، فعّل
cache_implementation="quantized"في الـgenerate أو استخدم paged attention في vLLM. - Fine-tuning أصعب. ما تقدرش تحدّث أوزان NF4 مباشرة لأنها مش differentiable بدقة كافية. الحل القياسي: QLoRA. تجمّد الـbase وتدرّب LoRA adapters فوقها بـbf16. الذاكرة الكلية تفضل قريبة من الـinference.
- الإجابات بتختلف قليلاً. حتى على temperature=0، نفس الـprompt ممكن يدّيك ترتيب جمل مختلف بين FP16 وNF4 في 5 إلى 8% من الحالات. لو عندك golden tests على الـoutput الحرفي، اختبر قبل ما تنشر في إنتاج.
متى لا تستخدم Quantization 4-bit
- عندك ميزانية GPU كافية أصلاً. لو شركتك مشغّلة H100 cluster، FP16 أبسط وأسرع. ما فيش سبب تضيف complexity في الـpipeline.
- الموديل أصغر من 7B. تأثير الـquantization على الـperplexity بيكبر مع الموديلز الصغيرة. Llama 3 8B في NF4 ممكن يخسر ~3% accuracy على MMLU. القاعدة: كل ما الموديل أكبر، الـquantization أرخص.
- دقة عالية مطلوبة (طب، قانون، مالية). الـ0.5 نقطة في MMLU ممكن تعني فرق إجابة حرجة. اختبر على benchmark تخصّصك قبل القرار، مش على benchmarks عامة.
- Production بـlatency < 200ms p50. دقّة الـCUDA kernels للـ4-bit في bitsandbytes أبطأ من FP16 المحسّن في TensorRT-LLM. لو محتاج ultra-low latency، استخدم FP8 على H100 بدل NF4، أو AWQ مع TensorRT-LLM.
الخطوة التالية
افتح nvidia-smi وشوف الـVRAM المتاح عندك. لو أقل من 48GB، نزّل Llama 3.1 8B بـNF4 وقيس الفرق على prompts شغلك الحقيقي. لو فوق 48GB، جرّب 70B بنفس الكود اللي فوق. سجّل ثلاث أرقام قبل وبعد: perplexity على عيّنة من بياناتك، latency على prompt متوسط، وVRAM الفعلي. لو الفرق في الجودة أقل من 1% والتوفير في الذاكرة 4×، عندك قرار سهل.
المصادر
- Dettmers, T. et al. "QLoRA: Efficient Finetuning of Quantized LLMs", NeurIPS 2023. arxiv.org/abs/2305.14314
- Hugging Face Blog: "Making LLMs even more accessible with bitsandbytes, 4-bit quantization and QLoRA". huggingface.co/blog/4bit-transformers-bitsandbytes
- Hugging Face Transformers documentation: bitsandbytes integration. huggingface.co/docs/transformers/quantization/bitsandbytes
- "Self-Hosting LLaMA 3.1 70B Affordably", Abhinand on Medium. abhinand05.medium.com
- "Quantization Explained: Run 70B Models on Consumer GPUs", SitePoint. sitepoint.com
- "Local LLM Hardware Guide 2026", PromptQuorum. promptquorum.com