সরাসরি পর্বে যান
পর্ব ৫৯ · ৯১-এর মধ্যে · মডিউল ৯

ছোট আর দ্রুত model — quantization, distillation আর pruning

Smaller and Faster Models: Quantization, Distillation and Pruning

Taught in Bangla, this lesson computes model memory in fp32, bf16, int8 and int4, derives scale and zero point for symmetric and asymmetric quantization, and shows in in-page NumPy how outliers hurt per-tensor scales. Real runs measure dynamic int8 on CPU and 8-bit and 4-bit bitsandbytes loading on the GPU, distil Bangla BERT into a small BiGRU with the T² temperature factor, and compare magnitude, structured and unstructured pruning for actual speed.

এই পর্বে যা শিখবেন

  • fp32, bf16, int8 আর int4-এ একটি model-এর memory হাতে হিসাব করা
  • Scale আর zero point হাতে বের করা; symmetric বনাম asymmetric, per-channel বনাম per-tensor
  • PyTorch-এ dynamic int8 আর bitsandbytes-এ 8/4-bit লোড করে আকার, গতি আর মান মাপা
  • Knowledge distillation-এর loss, temperature আর T² গুণকের কারণ
  • Magnitude pruning, structured বনাম unstructured, আর কোনটিতে আসলে গতি বাড়ে

পর্ব ৫৫ · Parameter-efficient fine-tuning-এ QLoRA 4-bit base ব্যবহার করেছিল, কিন্তু 4-bit আসলে কী, তা খুলে দেখা হয়নি। পর্ব ৪৮ · BERT পরিবার-এর Bangla BERT ভালো ফল দেয়, কিন্তু ৬২৭ MiB আর CPU-তে ধীর। এই পর্বে তিনটি কৌশলে model ছোট আর দ্রুত করব, আর প্রতিবার পর্ব ৫৮ · LLM মূল্যায়ন-এর নিয়মে মাপব মান কতটা টিকল। ১৬-bit সংখ্যার গঠন পর্ব ১২ · GPU performance-এ আছে। Desktop-এর run লেখার সময়ে (২০২৬ সালের নভেম্বর) এই version-এ: Python 3.12, torch 2.15.0, transformers 5.22.2, bitsandbytes 0.50.2, scikit-learn 1.9.1. কোন run GPU-তে আর কোনটি CPU-তে, তা পাশে লেখা আছে।

১ · কেন ছোট model — প্রতি parameter-এ কত byte

মুদি দোকানের খাতায় দোকানি ৪৭.৩৮ টাকা লেখেন না, লেখেন ৪৭ টাকা। প্রতিটি হিসাবে কয়েক পয়সা হারায়, কিন্তু খাতা অনেক সহজ হয়, আর মাসের শেষে মোট প্রায় একই থাকে। Model-এর ওজনেও একই কাজ করা যায়। এর নাম quantization.

ছোট model কেন দরকার? তিনটি খরচ। এক, memory: পুরো model GPU বা ফোনের RAM-এ ধরতে হবে। দুই, latency: একটি উত্তরে কত সময়। LLM-এ প্রতিটি নতুন token-এর জন্য সব ওজন memory থেকে একবার পড়তে হয়, তাই ওজন ছোট হলে পড়াও দ্রুত। তিন, টাকা আর বিদ্যুৎ: একই server-এ বেশি অনুরোধ। এর তিনটি উত্তর:

Quantization

একই সংখ্যক ওজন, প্রতিটি কম bit-এ। ৩২ bit থেকে ৮ বা ৪ bit. সাধারণত training ছাড়াই করা যায়।

Distillation

বড় teacher-এর উত্তর দেখে একটি ছোট student নতুন করে শেখানো। Training লাগে।

Pruning

কম দরকারি ওজন শূন্য করা, বা পুরো neuron, head বা layer কেটে ফেলা। প্রায়ই পরে একটু training লাগে।

প্রথম হিসাবটি সরল: parameter-এর সংখ্যা × প্রতিটির byte. fp32-এ ৪ byte, bf16 বা fp16-এ ২, int8-এ ১, int4-এ আধা byte. তবে quantize করা ওজনের সঙ্গে scale-ও রাখতে হয়, আর কিছু অংশ (embedding, norm) প্রায়ই quantize করা হয় না। নিচের stepper-এ Qwen2.5-0.5B (Apache 2.0), যার parameter পর্ব ৫৫-এ গোনা হয়েছিল:

হিসাবের খাতা
কী
হিসাব
MiB
  1. গোনা. ওজনের ফাইল থেকে: Linear layer-এ ৩৫.৮ কোটি, embedding-এ ১৩.৬ কোটি (input আর output-এ একই, tied), বাকি norm আর bias মাত্র ৭১,৫৫২টি।
  2. fp32. প্রতিটি ৪ byte: ১,৮৮৪.৬ MiB. ২২০ byte-এ এক MiB.
  3. bf16. অর্ধেক: ৯৪২.৩ MiB. transformers-এর get_memory_footprint() মেপেছে ৯৪২, হুবহু।
  4. int8, কাগজে. সব ওজন ১ byte হলে ৪৭১.১ MiB, bf16-এর অর্ধেক।
  5. bitsandbytes 8-bit, বাস্তবে. শুধু Linear int8-এ যায়; embedding আর norm bf16-এ থাকে। ৩৪১.২ + ২৫৯.৮ = ৬০১.০, মাপা ৬০১। অর্ধেক নয়, ৩৬% কম।
  6. int4, কাগজে. আধা byte: ২৩৫.৬ MiB, fp32-এর আট ভাগের এক ভাগ।
  7. NF4, বাস্তবে. Linear আধা byte, বাকিটা bf16: ৪৩০.৪, মাপা ৪৩০। GPU-তে লোডের পর ৪৩৬, কারণ ৬৪-এর block-এর scale-ও লাগে। ছোট model-এ embedding-এর ভাগ বড়, তাই bf16-এর তুলনায় সাশ্রয় চার গুণ নয়, প্রায় ২.২ গুণ।

২ · Quantization-এর মূল সূত্র — scale আর zero point

ধরুন একটি tensor-এর মান −০.৫ থেকে ২.০৫-এর মধ্যে। int8-এ মাত্র ২৫৬টি ঘর। কাজ হলো এই দশমিক পরিসরকে ২৫৬টি সমান ধাপে ভাগ করা। প্রতিটি ধাপের মাপ scale s. আর আসল ০.০ কোন পূর্ণসংখ্যায় পড়বে, সেটি zero point z. সূত্র:

q = clip(round(x / s) + z, qmin, qmax)  ·   x̂ = (q − z) · s

এখানে x আসল দশমিক মান, q পূর্ণসংখ্যা, x̂ (“এক্স-হ্যাট”) ফিরিয়ে আনা মান, আর clip মানে সীমার বাইরে গেলে সীমায় আটকে দেওয়া। q থেকে x̂ ফেরানোকে বলে dequantization. Asymmetric পদ্ধতিতে s = (max − min) / (qmax − qmin) আর z = round(qmin − min / s). Symmetric পদ্ধতিতে z = ০ আর s = max|x| / ১২৭; হিসাব সহজ, কিন্তু পরিসর একদিকে হেলে থাকলে ঘর নষ্ট হয়।

Affine quantization: float range mapped to an integer grid দশমিক x (float) −0.50.01.2342.05 ÷ s, round, + z 050173255 uint8 · asymmetric: s = 0.01, z = 50 −127−320127 অব্যবহৃত int8 · symmetric: s = 2.05/127, z = 0
Asymmetric-এ পুরো [min, max] পরিসর ২৫৬টি ঘরে ছড়ায়, আর ০.০ ঠিক একটি পূর্ণসংখ্যায় (৫০) পড়ে। Symmetric-এ ঋণাত্মক দিকে −০.৫-এর বেশি মান নেই, তাই −১২৭ থেকে −৩২-এর ৯৬টি ঘর কখনো কাজে লাগে না।
খাতায় হিসাব
ধাপ
হিসাব
ফল
  1. পরিসর. Tensor-এর সবচেয়ে ছোট মান −০.৫, সবচেয়ে বড় ২.০৫। মোট বিস্তার ২.৫৫। uint8-এ ঘর ০ থেকে ২৫৫।
  2. Scale. ২.৫৫ বিস্তারকে ২৫৫টি ধাপে ভাগ: s = ০.০১। অর্থাৎ পাশাপাশি দুটি পূর্ণসংখ্যার মধ্যে দশমিকের পার্থক্য ০.০১।
  3. Zero point. min যেন ঘর ০-এ পড়ে: z = round(০ − (−০.৫)/০.০১) = ৫০। আসল ০.০ এখন ঠিক পূর্ণসংখ্যা ৫০; padding বা ReLU-র শূন্য তাই কোনো ত্রুটি ছাড়া ফেরে।
  4. Quantize. ১.২৩৪ ÷ ০.০১ = ১২৩.৪, round করে ১২৩, z যোগ করে q = ১৭৩। এক byte-এ রাখা যায়।
  5. Dequantize. (১৭৩ − ৫০) × ০.০১ = ১.২৩। ত্রুটি ০.০০৪, যা s/2 = ০.০০৫-এর কম। Round-এর ত্রুটি কখনো আধা ধাপের বেশি হয় না (সীমার ভেতরে থাকলে)।
  6. Symmetric-এ. z = ০, s = ২.০৫/১২৭ ≈ ০.০১৬১৪; ১.২৩৪/s ≈ ৭৬.৪৫, তাই q = ৭৬, x̂ ≈ ১.২২৬৮, ত্রুটি ০.০০৭২, প্রায় দ্বিগুণ। কারণ −১২৭ থেকে −৩২-এর ঘরগুলো অব্যবহৃত, ধাপ মোটা।

এবার হাজারটি সংখ্যায় একই কাজ চালিয়ে দেখুন। দুই ধরনের ডেটা: শূন্যকেন্দ্রিক ওজন, আর ReLU-র পরের activation, যার সব মান শূন্য বা ধনাত্মক।

import numpy as np
rng = np.random.default_rng(42)
w = rng.normal(0, 0.05, 1000)                  # একটি layer-এর ওজনের মতো: শূন্যকেন্দ্রিক
a = np.maximum(rng.normal(0.3, 0.4, 1000), 0)  # ReLU-এর পরের activation: সব ≥ 0

def quant(x, bits, sym):
    if sym:                                    # symmetric: zero point = 0
        qmax = 2 ** (bits - 1) - 1             # int8-এ 127
        s, z, lo, hi = np.abs(x).max() / qmax, 0, -qmax, qmax
    else:                                      # asymmetric: [min, max] পুরো grid-এ
        lo, hi = 0, 2 ** bits - 1              # uint8-এ 0..255
        s = (x.max() - x.min()) / (hi - lo)
        z = int(np.round(lo - x.min() / s))
    q = np.clip(np.round(x / s) + z, lo, hi)   # float → পূর্ণসংখ্যা
    return q, s, z

def report(name, x, bits, sym):
    q, s, z = quant(x, bits, sym)
    xh = (q - z) * s                           # dequantize: পূর্ণসংখ্যা → float
    err = np.abs(x - xh)
    used = len(np.unique(q))
    print(f"{name:6s} {bits} {'sym ' if sym else 'asym'} │ {s:.5f} │ {z:3d} │ {used:3d}"
          f" │ {err.max():.5f} │ {s / 2:.5f} │ {np.sqrt((err**2).mean()) / x.std():.4f}")

print("ডেটা  bit মোড  │ scale s │ z   │ ঘর  │ max err │ s/2     │ rel RMSE")
for bits in (8, 4):
    report("weight", w, bits, True)
    report("weight", w, bits, False)
    report("act", a, bits, True)
    report("act", a, bits, False)
q, s, z = quant(a, 8, False)
print("activation-এ শূন্য:", int((a == 0).sum()), "টি │ ফিরে ঠিক 0.0?",
      bool(np.all((q[a == 0] - z) * s == 0)))

আউটপুট (পাতার Python-এ চালানো):

ডেটা  bit মোড  │ scale s │ z   │ ঘর  │ max err │ s/2     │ rel RMSE
weight 8 sym  │ 0.00144 │   0 │ 169 │ 0.00072 │ 0.00072 │ 0.0085
weight 8 asym │ 0.00134 │ 136 │ 179 │ 0.00067 │ 0.00067 │ 0.0080
act    8 sym  │ 0.01154 │   0 │ 106 │ 0.00576 │ 0.00577 │ 0.0091
act    8 asym │ 0.00575 │   0 │ 192 │ 0.00287 │ 0.00287 │ 0.0045
weight 4 sym  │ 0.02606 │   0 │  14 │ 0.01301 │ 0.01303 │ 0.1543
weight 4 asym │ 0.02276 │   8 │  16 │ 0.01135 │ 0.01138 │ 0.1329
act    4 sym  │ 0.20939 │   0 │   8 │ 0.10437 │ 0.10469 │ 0.1649
act    4 asym │ 0.09771 │   0 │  16 │ 0.04886 │ 0.04886 │ 0.0774
activation-এ শূন্য: 247 টি │ ফিরে ঠিক 0.0? True

প্রতিটি সারিতে max err আর s/2 প্রায় সমান: round-এর ত্রুটি কখনো আধা ধাপ ছাড়ায় না। শূন্যকেন্দ্রিক ওজনে symmetric আর asymmetric প্রায় সমান (০.০০৮৫ বনাম ০.০০৮০)। কিন্তু activation-এ symmetric ২৫৫টি ঘরের মাত্র ১০৬টি ব্যবহার করে, কারণ ঋণাত্মক দিক ফাঁকা; asymmetric ১৯২টি ব্যবহার করে, আর ত্রুটি অর্ধেক (০.০০৯১ থেকে ০.০০৪৫)। 4-bit-এ ঘর মাত্র ১৬টি, তাই ত্রুটি প্রায় ১৭ গুণ (০.১৩ থেকে ০.১৬)। শেষ লাইন: ReLU-র ২৪৭টি শূন্য asymmetric-এও ঠিক ০.০ হয়ে ফিরেছে, কারণ zero point নিজেই একটি পূর্ণসংখ্যা।

৩ · Per-tensor, per-channel আর outlier

পুরো tensor-এর জন্য একটি scale হলো per-tensor quantization. প্রতিটি সারি বা কলামের নিজের scale হলে per-channel; আর ৬৪ বা ১২৮টি মানের প্রতিটি ছোট দলের নিজের scale হলে group-wise বা block-wise (পর্ব ৫৫-এর NF4 এভাবেই ৬৪-এর block নেয়)। Scale যত বেশি, বাড়তি জায়গা তত বেশি, কিন্তু ত্রুটি তত কম।

কেন এটি এত জরুরি? ক্লাসের ৩০ জনের নম্বর একটি স্কেলে আঁকতে গিয়ে যদি একজনের নম্বর ভুলে ১০০০ লেখা থাকে, বাকিদের সবাই শূন্যের কাছে জড়ো হয়ে যায়। Dettmers ও সহলেখকেরা (LLM.int8(), ২০২২) দেখান, বড় transformer-এ কয়েকটি নির্দিষ্ট hidden মাত্রায় খুব বড় activation (outlier feature) নিয়মিত দেখা দেয়, আর তাঁদের পরীক্ষায় প্রায় ৬৭০ কোটি parameter থেকে বড় model-এ এই সমস্যা প্রকট হয়েছিল। নিচের কোডে ৬৪টি token × ১৬টি feature-এর একটি activation, যার feature ৫ বাকিদের প্রায় ৬০ গুণ বড়:

import numpy as np
rng = np.random.default_rng(42)
X = rng.normal(0, 1, (64, 16))                 # 64টি token × 16টি feature (activation)
X[:, 5] *= 60                                  # feature 5: outlier, বাকিদের চেয়ে ~60 গুণ বড়
W = rng.normal(0, 0.1, (16, 8))                # পরের Linear layer-এর ওজন
Y = X @ W                                      # আসল (float) output

def q8(x, axis=None):                          # symmetric int8, scale নির্দিষ্ট axis ধরে
    s = np.abs(x).max(axis=axis, keepdims=axis is not None) / 127
    return np.clip(np.round(x / s), -127, 127) * s

def rel_err(Xh, cols=slice(None)):
    return np.linalg.norm(Xh[:, cols] - X[:, cols]) / np.linalg.norm(X[:, cols])

normal = [c for c in range(16) if c != 5]
cases = {
    "per-tensor": q8(X),
    "per-token (সারি)": q8(X, axis=1),
    "per-feature (কলাম)": q8(X, axis=0),
}
out = np.arange(16) == 5                       # LLM.int8 ধারণা: outlier কলাম float-এ
cases["outlier আলাদা"] = q8(np.where(out, 0, X)) + np.where(out, X, 0)
print("scale per-tensor:", round(np.abs(X).max() / 127, 4),
      "│ সাধারণ কলামের |x| সর্বোচ্চ:", round(np.abs(X[:, normal]).max(), 2))
print(f"{'পদ্ধতি':20s} │ সাধারণ কলামে ত্রুটি │ output Y-তে ত্রুটি")
for name, Xh in cases.items():
    ey = np.linalg.norm(Xh @ W - Y) / np.linalg.norm(Y)
    print(f"{name:20s} │ {rel_err(Xh, normal):18.4f} │ {ey:.4f}")
z = np.round(X[:, normal] / (np.abs(X).max() / 127))
print("per-tensor-এ সাধারণ কলামের কতগুলো মান 0-তে নামল:", f"{(z == 0).mean():.0%}")

আউটপুট (পাতার Python-এ চালানো):

scale per-tensor: 1.7237 │ সাধারণ কলামের |x| সর্বোচ্চ: 3.18
পদ্ধতি               │ সাধারণ কলামে ত্রুটি │ output Y-তে ত্রুটি
per-tensor           │             0.4991 │ 0.0350
per-token (সারি)     │             0.1572 │ 0.0115
per-feature (কলাম)   │             0.0059 │ 0.0075
outlier আলাদা        │             0.0076 │ 0.0005
per-tensor-এ সাধারণ কলামের কতগুলো মান 0-তে নামল: 63%

Per-tensor-এ scale ১.৭২, অথচ সাধারণ কলামের সবচেয়ে বড় মান ৩.১৮: এক ধাপেই প্রায় পুরো পরিসর। ফলে সাধারণ মানগুলোর ৬৩% round হয়ে ০, আপেক্ষিক ত্রুটি ০.৫০। Per-token (প্রতি সারির scale) সামান্য সাহায্য করে (০.১৬), কারণ প্রতিটি সারিতেই outlier কলাম আছে। Per-feature (প্রতি কলামের scale) outlier-কে নিজের কলামে আটকে রাখে: ০.০০৫৯। Output Y-এর ত্রুটি per-tensor-এও মাত্র ০.০৩৫, কারণ Y-এর বড় অংশ outlier কলাম থেকেই আসে; কিন্তু বাকি ১৫টি feature-এর তথ্য প্রায় মুছে গেছে, যা পরের layer-এ বড় ক্ষতি হতে পারে। Outlier কলাম float-এ রেখে বাকিটা int8 (LLM.int8()-এর ধারণা) Y-তে সবচেয়ে কম ত্রুটি দেয়: ০.০০০৫।

বাস্তবের প্রচলিত পদ্ধতিগুলো এই সমস্যারই নানা সমাধান। নিচের বিবরণ প্রতিটির মূল প্রবন্ধ বা নথি থেকে, সংক্ষেপে:

পদ্ধতিমূল কৌশলকোথায়
LLM.int8() (Dettmers ও সহলেখক, ২০২২)সারি-কলাম ধরে int8; outlier মাত্রাগুলো আলাদা করে 16-bit-এ গুণbitsandbytes-এর 8-bit লোড
GPTQ (Frantar ও সহলেখক, ২০২২)একটি ছোট calibration ডেটায় layer-এর output যত কম বদলায়, সেভাবে ওজন একে একে 3–4 bit-এ; দ্বিতীয়-ক্রমের তথ্য ব্যবহার করে বাকি ওজন সংশোধনআগে থেকে quantize করা checkpoint
AWQ (Lin ও সহলেখক, ২০২৩)Activation বড় এমন অল্প কিছু “গুরুত্বপূর্ণ” channel-কে quantize-এর আগে scale করে রক্ষা4-bit weight-only
GGUF (llama.cpp প্রকল্প)একটি ফাইল ফরম্যাট: ওজন, tokenizer আর metadata একসঙ্গে; block-ভিত্তিক নানা quantization ধরনCPU আর ল্যাপটপে local LLM

দুটি শব্দজোড়া মনে রাখুন। Post-training quantization (PTQ): শেখা model-কে পরে quantize করা, training ছাড়া; ওপরের সবগুলোই এই দলে। Quantization-aware training (QAT): training-এর সময়ই rounding-এর প্রভাব নকল করা, যাতে model তা সামলাতে শেখে; খরচ বেশি, কম bit-এ মান সাধারণত ভালো। আর activation-এর scale যদি আগে থেকে calibration ডেটায় ঠিক করা থাকে তবে static, প্রতিটি batch-এ নতুন করে মাপা হলে dynamic.

৪ · PyTorch-এ dynamic int8, CPU-তে — সত্যিকারের run

প্রথমে একটি teacher বানালাম, যা এই পর্বের সব পরীক্ষায় লাগবে: sagorsarker/bangla-bert-base (MIT, do_lower_case=False), পর্ব ৪২ · Text classification-এর Sazzed-এর বাংলা YouTube মন্তব্যে (Mendeley Data, CC BY 4.0), প্রথম split (train ৭,৫১০, validation ১,০৫৬, test ২,১৪৩), পর্ব ৫৫-এর full fine-tuning-এর একই রেসিপিতে, GPU-তে ৩ epoch, ১০০.৯ সেকেন্ড। Test macro-F1 ০.৯৪২৫, পর্ব ৫৫-এর সংখ্যার সঙ্গে হুবহু।

এবার এই model-কে CPU-তে dynamic int8-এ নিলাম। Dynamic মানে: Linear layer-এর ওজন আগে থেকেই int8-এ রাখা হয়, আর activation-এর scale প্রতিটি batch চলার সময় মেপে quantize করা হয়, তাই calibration ডেটা লাগে না। মূল লাইন একটিই (script থেকে ছোট করা):

model = AutoModelForSequenceClassification.from_pretrained("teacher").eval()
qmodel = torch.ao.quantization.quantize_dynamic(model, {torch.nn.Linear}, dtype=torch.qint8)

প্রতিটি model-এ মাপা হয়েছে: state_dict-এর আকার, ২০০টি test মন্তব্যে এক এক করে (batch ১) সময়ের median, পুরো ২,১৪৩টি test ৬৪-এর batch-এ, আর macro-F1. CPU-তে ৩টি thread, warm-up-এর পর। সত্যিকারের run, JSON লাইন থেকে সাজানো:

Modelআকার (MiB)batch ১ (ms)পুরো test (s)Macro-F1fp32-এর সঙ্গে একই সিদ্ধান্ত
fp32৬২৭.২২৪.৬২৩.৫০.৯৪২৫২,১৪৩ / ২,১৪৩
dynamic int8৩৮২.৬৩৮.৫১৮.৮০.৯৪২৫২,১৪৩ / ২,১৪৩

আকার: ৬২৭ থেকে ৩৮৩ MiB, চার ভাগের এক ভাগ নয়। কারণ এই model-এর প্রায় অর্ধেক ওজন embedding-এ (৭.৮ কোটি), যা nn.Linear নয়, তাই fp32-এই থেকেছে; শুধু encoder-এর ৮.৫ কোটি Linear ওজন int8 হয়েছে। (Quantize করা Linear-এর ওজন সাধারণ parameter হিসেবে গোনা হয় না, তাই parameter-গণনা এখানে বিভ্রান্তিকর।) মান: macro-F1 হুবহু এক, আর ২,১৪৩টির প্রতিটিতে সিদ্ধান্ত fp32-এর মতো; তবু logit-এ সর্বোচ্চ পার্থক্য ০.৬১, অর্থাৎ output বদলেছে, শুধু সিদ্ধান্ত বদলানোর মতো নয়।

সময়, সৎভাবে: ৬৪-এর batch-এ int8 প্রায় ২০% দ্রুত (২৩.৫ থেকে ১৮.৮ সেকেন্ড), কিন্তু এক এক করে চালালে উল্টো, ২৪.৬ থেকে ৩৮.৫ ms. ছোট input-এ প্রতিবার activation quantize আর dequantize করার বাড়তি খরচ গুণের সাশ্রয়ের চেয়ে বেশি হয়ে যায়। এটি এই CPU, এই thread-সংখ্যা আর এই PyTorch version-এর একটি run; অন্য যন্ত্রে ফল উল্টাতে পারে। তাই নিজের যন্ত্রে, নিজের batch আকারে মাপুন।

সাবধান: আমাদের run-এ torch.ao.quantization নিজেই একটি DeprecationWarning দিয়েছে: eager mode-এর এই API সরিয়ে torchao library-তে নেওয়া হচ্ছে, আর quantized tensor বানানোর পুরনো function-গুলোর জন্য একটি UserWarning-ও এসেছে। torch 2.15.0-তে এটি এখনো কাজ করেছে, কিন্তু নতুন কোডে PyTorch-এর নথিতে বলা পথ (torchao) দেখে নিন। পুরনো টিউটোরিয়াল আর AI-এর লেখা কোড প্রায়ই পুরনো API দেয়।

৫ · bitsandbytes-এ 8-bit আর 4-bit — একটি ছোট LLM

এবার decoder LLM: Qwen/Qwen2.5-0.5B (base, Apache 2.0, revision 060db64). bitsandbytes দিয়ে তিনভাবে লোড: bf16, 8-bit (LLM.int8()) আর 4-bit NF4, double quantization-সহ। মাপা হয়েছে memory footprint, peak GPU memory, উপেন্দ্রকিশোরের তিনটি গল্পে (bn.wikisource, পাতায় PD-India ট্যাগ) perplexity, আর greedy generation-এর গতি। GPU-তে, প্রতিটি precision আলাদা process-এ:

bnb8 = BitsAndBytesConfig(load_in_8bit=True)
bnb4 = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
           bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=torch.bfloat16)
model = AutoModelForCausalLM.from_pretrained(path, quantization_config=bnb4, device_map={"": 0})
# perplexity: 8,192 token, ৫১২-এর আলাদা জানালায় (script থেকে ছোট করা)
PrecisionFootprint (MiB)Peak (MiB)PerplexityGeneration (token/s)
bf16৯৪২১,৮৪৫৩.৮৫৫৪.৮
8-bit (LLM.int8())৬০১১,৫০৫৩.৮৮৯.৩
4-bit NF4৪৩০১,৩৩৯৪.২১৪১.১

একই prompt (“শিয়াল পণ্ডিত একদিন বনের ধারে বসে ভাবছিল,”), greedy decoding, seed ০; script প্রথম ৪০টি token decode করে print করে, তাই শেষের ভাঙা অক্ষর “…” দিয়ে বদলানো আর লাইনের শুরুতে precision-এর নাম যোগ করা হয়েছে। সত্যিকারের output:

bf16  GEN:  এবং একদিন বনের ধারে বসে ভাবছিল, এবং এ…
int8  GEN:  এবং একটি পরিবর্তন করে এবং একটি পরিবর্…
nf4   GEN:  এই বিবেচনা সম্পর্কে সম্পর্কে সম্পর্কে স…

Memory: অংশ ১-এর stepper-এর হিসাবের সঙ্গে হুবহু: ৯৪২, ৬০১, ৪৩০। Peak-এ প্রায় ৯০০ MiB বাড়তি তিনটিতেই, যা ৫১২ token-এর activation আর logits (১,৫১,৯৩৬ শব্দের vocabulary!) থেকে; quantization সেটি কমায় না। মান: 8-bit-এ perplexity প্রায় অপরিবর্তিত (৩.৮৫ থেকে ৩.৮৮), 4-bit-এ ৯% বেড়েছে (৪.২১)। Perplexity এখানে প্রতি token-এ; এই tokenizer বাংলার একটি শব্দকে গড়ে প্রায় ৫.৮ টুকরোয় ভাঙে, আর একই শব্দের পরের টুকরো অনুমান সহজ, তাই সংখ্যাটি ছোট দেখায়। Wikisource-এর এই গল্প Qwen-এর training ডেটায় ছিল কি না, তা আমরা জানি না; “held-out” মানে শুধু আমরা এগুলো অন্য কোনো কাজে ব্যবহার করিনি।

গতি: 8-bit এখানে প্রায় ছয় গুণ ধীর। LLM.int8() outlier মাত্রা আলাদা করে দুই রকম গুণ করে; একটি একটি token-এর generation-এ এই বাড়তি কাজ গুণের সাশ্রয় ছাপিয়ে যায়। NF4 bf16-এর চেয়ে প্রায় ২৫% ধীর, কারণ প্রতিটি গুণের আগে ওজন dequantize হয়। এই ছোট model-এ GPU-তে memory-র চাপ ছিল না; বড় model-এ, যেখানে ওজন পড়াই প্রধান খরচ, ফল ভিন্ন হতে পারে। উত্তর: greedy-তেও তিনটি উত্তর তিন রকম, কারণ প্রথম কয়েকটি token-এর ছোট পার্থক্য পরের সব token-এর পথ বদলায়। আর base model-টি তিনটিতেই পুনরাবৃত্তি করছে; উত্তরের মান বিচারের জন্য পর্ব ৫৮-এর মতো আলাদা মূল্যায়ন লাগবে, শুধু perplexity নয়।

৬ · Knowledge distillation — teacher, student আর temperature

অভিজ্ঞ ডাক্তার রোগ নির্ণয়ে শুধু “টাইফয়েড” বলেন না। বলেন, “সম্ভবত টাইফয়েড, তবে ডেঙ্গুও হতে পারে, ম্যালেরিয়ার সম্ভাবনা কম।” নবীন ডাক্তার এই দ্বিধা থেকেও শেখেন: কোন রোগগুলো একে অপরের কাছাকাছি। Model-এর বেলায় এই নরম উত্তর হলো softmax-এর পুরো বণ্টন, শুধু সবচেয়ে বড়টি নয়। বাজারঘরের রিভিউ “ডেলিভারি দেরিতে, কিন্তু জিনিস ভালো” পড়ে একটি ভালো teacher হয়তো বলে ভালো ০.৬, খারাপ ০.৪; hard label শুধু বলে “ভালো”।

Knowledge distillation-এর আধুনিক রূপ Hinton, Vinyals ও Dean-এর ২০১৫ সালের প্রবন্ধে; বড় model-এর আচরণ ছোট model-এ নেওয়ার ধারণা আরও আগের (Buciluǎ, Caruana ও Niculescu-Mizil, ২০০৬, “model compression”). KL divergence আর nn.KLDivLoss-এর ফাঁদ পর্ব ১৩ · Loss function-এ আছে; এখানে পুরো পদ্ধতি। Teacher-এর logit zt আর student-এর zs-কে একটি temperature T দিয়ে ভাগ করে softmax নেওয়া হয়: pT = softmax(zt/T), qT = softmax(zs/T). T > ১ হলে বণ্টন নরম হয়, ছোট probability-গুলো চোখে পড়ে। এগুলোই soft target. Loss:

L = α · CE(y, softmax(zs)) + (1 − α) · T² · KL(pT ‖ qT)

প্রথম অংশ সাধারণ cross-entropy, আসল label y-এর সঙ্গে, T = ১-এ। দ্বিতীয় অংশ teacher-এর নরম বণ্টনের সঙ্গে মিল। α দুটির ভার ঠিক করে। আর T² কেন? Student-এর logit-এর সাপেক্ষে KL অংশের gradient হলো (qT − pT)/T. বড় T-এ q আর p দুটিই সমান বণ্টনের কাছে যায়, তাই পার্থক্যও মোটামুটি 1/T হারে ছোট হয়; সব মিলিয়ে gradient প্রায় 1/T² হারে কমে। T² দিয়ে গুণ করলে মাপ মোটামুটি স্থির থাকে, আর T বদলালে α নতুন করে খুঁজতে হয় না। Hinton ও সহলেখকেরা প্রবন্ধে ঠিক এই কারণ লিখেছেন।

এই ধাপে
ধাপ
হিসাব
ফল
  1. Temperature. তিন শ্রেণি: ভালো, মাঝারি, খারাপ। দুই model-এর logit T = ২ দিয়ে ভাগ; পার্থক্যগুলো অর্ধেক হয়।
  2. Teacher-এর নরম বণ্টন. e-এর ঘাত নিয়ে যোগফল ৬.৭৪ দিয়ে ভাগ: [০.৬৬৫, ০.২৪৫, ০.০৯০]। T = ১-এ এটি [০.৮৬৭, ০.১১৭, ০.০১৬] হতো; “খারাপ”-এর ছোট সম্ভাবনা এখন চোখে পড়ে।
  3. Student-এর বণ্টন. একইভাবে [০.৪৮৪, ০.৩৭৭, ০.১৩৯]। Student “মাঝারি”-কে বেশি দিচ্ছে।
  4. KL. প্রতিটি শ্রেণিতে p·ln(p/q): ০.২১১৩, −০.১০৫৮, −০.০৩৮৯। যোগ ০.০৬৬৬। কিছু পদ ঋণাত্মক হতে পারে, কিন্তু মোট KL কখনো ঋণাত্মক নয়।
  5. T² গুণ. ৪ × ০.০৬৬৬ = ০.২৬৬৪। এটি KD অংশ।
  6. মোট. আসল label “ভালো”: T = ১-এ student-এর probability ০.৫৯২, CE = −ln ০.৫৯২ = ০.৫২৩৯। α = ০.৫-এ L = ০.২৬২০ + ০.১৩৩২ = ০.৩৯৫২।
  7. Gradient. KD অংশের gradient T(qT − pT) = [−০.৩৬২, ০.২৬৫, ০.০৯৭]: “ভালো”-র logit বাড়াতে, “মাঝারি” কমাতে বলে। যোগফল শূন্য, softmax-এর gradient-এর মতোই।

এবার চারটি temperature-এ loss আর gradient, আর gradient-এর সূত্রটি finite difference দিয়ে যাচাই:

import numpy as np
np.set_printoptions(precision=3, suppress=True)
zt = np.array([3.0, 1.0, -1.0])                # teacher logits: ভালো, মাঝারি, খারাপ
zs = np.array([2.0, 1.5, -0.5])                # student logits
y = 0                                          # আসল (hard) label: "ভালো"

def softmax(z, T=1.0):
    e = np.exp((z - z.max()) / T)
    return e / e.sum()

def kd_loss(zs, T):                            # T² · KL(p_T ‖ q_T)
    p, q = softmax(zt, T), softmax(zs, T)
    return T**2 * np.sum(p * (np.log(p) - np.log(q)))

print("T   │ teacher-এর p_T      │ KD     │ |g|    │ T² ছাড়া │ যাচাই")
for T in (1, 2, 4, 8):
    p, q = softmax(zt, T), softmax(zs, T)
    g = T * (q - p)                            # বিশ্লেষণী gradient: d loss / d zs
    eps, num = 1e-6, np.zeros(3)
    for i in range(3):                         # finite difference দিয়ে যাচাই
        d = np.zeros(3); d[i] = eps
        num[i] = (kd_loss(zs + d, T) - kd_loss(zs - d, T)) / (2 * eps)
    print(f"T={T} │ {p} │ {kd_loss(zs, T):.4f} │ {np.linalg.norm(g):.4f}"
          f" │ {np.linalg.norm(g) / T**2:.4f} │ {np.abs(g - num).max():.0e}")

T, alpha = 2, 0.5
ce = -np.log(softmax(zs)[y])                   # hard label-এর cross-entropy (T=1)
total = alpha * ce + (1 - alpha) * kd_loss(zs, T)
print(f"hard CE={ce:.4f} │ মোট = {alpha}·CE + {1 - alpha}·KD = {total:.4f}")

আউটপুট (পাতার Python-এ চালানো):

T   │ teacher-এর p_T      │ KD     │ |g|    │ T² ছাড়া │ যাচাই
T=1 │ [0.867 0.117 0.016] │ 0.1812 │ 0.3674 │ 0.3674 │ 7e-11
T=2 │ [0.665 0.245 0.09 ] │ 0.2664 │ 0.4590 │ 0.1147 │ 3e-10
T=4 │ [0.506 0.307 0.186] │ 0.2800 │ 0.4595 │ 0.0287 │ 1e-09
T=8 │ [0.419 0.326 0.254] │ 0.2708 │ 0.4405 │ 0.0069 │ 7e-09
hard CE=0.5239 │ মোট = 0.5·CE + 0.5·KD = 0.3952

যাচাই কলামে finite difference আর সূত্রের পার্থক্য ১০−৯-এর মাপে: gradient T(qT − pT) ঠিক। T² গুণসহ |g| চারটি temperature-এই ০.৩৭ থেকে ০.৪৬-এর মধ্যে; T² ছাড়া তা ০.৩৬৭ থেকে ০.০০৬৯-এ নেমে যায়, প্রায় 1/T² হারে। T = ৮-এ T² না দিলে soft target-এর অংশ hard CE-র পাশে প্রায় অদৃশ্য হতো। T = ২-এর সারি আর শেষ লাইন ওপরের stepper-এর হাতের হিসাবের সঙ্গে হুবহু: ০.২৬৬৪ আর ০.৩৯৫২।

৭ · সত্যিকারের distillation — Bangla BERT থেকে ছোট BiGRU

Teacher: অংশ ৪-এর Bangla BERT (১৬.৪ কোটি parameter, test macro-F1 ০.৯৪২৫)। Student: একটি ছোট BiGRU (পর্ব ২৯ · LSTM আর GRU): ৬৪ মাত্রার embedding, দুই দিকের GRU, mean আর max pooling, তারপর একটি Linear; মোট ৬৫,৭৮,১১৪টি parameter, teacher-এর ২৫ ভাগের এক ভাগ, বেশির ভাগই embedding-এ। একই tokenizer, একই split. Student CPU-তে ৮ epoch, Adam, lr ২ × ১০−৩; validation-এর সেরা epoch-এর ওজন test-এ। Teacher-এর logit একবার হিসাব করে রাখা হয়েছে, তাই distillation-এর সময় teacher আর চালাতে হয় না। প্রতিটি সেটিং তিনটি seed-এ (০, ১, ২); KD-তে α = ০.৫। সত্যিকারের run, JSON লাইন থেকে সাজানো:

Student যা থেকে শেখেLabelTest macro-F1 (৩ seed)গড় ± SD
শুধু hard label৭,৫১০০.৯১০০, ০.৯১১০, ০.৯১০৬০.৯১০৫ ± ০.০০০৫
KD, T = ২৭,৫১০০.৯১৬৪, ০.৯১৬৩, ০.৯১৮২০.৯১৭০ ± ০.০০১১
KD, T = ৪৭,৫১০০.৯২৫৯, ০.৯২০০, ০.৯২৫৮০.৯২৩৯ ± ০.০০৩৪
শুধু hard label১,০০০০.৮৩০৬, ০.৮৩৫২, ০.৮২৬৭০.৮৩০৮ ± ০.০০৪৩
KD, T = ২ (soft target সব ৭,৫১০টিতে)১,০০০০.৯১৩৯, ০.৯২৩৮, ০.৯২৪৯০.৯২০৯ ± ০.০০৬১

পুরো label-এ: তিনটি seed-এই KD hard label-এর চেয়ে ভালো, আর T = ৪-এ লাভ বেশি (০.৯১০৫ থেকে ০.৯২৩৯)। কারণটি মাপা যায়: teacher তার নিজের training মন্তব্যে প্রায় নিশ্চিত, T = ১-এ ৯৫.৮% মন্তব্যে সর্বোচ্চ probability ০.৯৯-এর বেশি, তাই soft target প্রায় hard label-এর মতোই। T = ৪-এ গড় সর্বোচ্চ probability নামে ০.৮৫৮-এ, আর “কতটা নিশ্চিত” তথ্যটি student পর্যন্ত পৌঁছায়। অল্প label-এ: মাত্র ১,০০০টি label থাকলে hard label-এর student ০.৮৩১, কিন্তু বাকি ৬,৫১০টি মন্তব্যে teacher-এর soft target দিলে ০.৯২১, প্রায় পুরো label-এর মতো। এখানে teacher আসলে label ছাড়া ডেটাকে label দিচ্ছে; distillation-এর বড় ব্যবহারিক লাভ প্রায়ই এটাই।

সীমা: student এখনো teacher-এর (০.৯৪২৫) নিচে। কয়েকটি run-এ সেরা epoch ছিল শেষটি (৮), তাই আরও epoch-এ ফাঁক কিছুটা কমতে পারত; তা মাপা হয়নি। তিনটি seed আর একটি split ছোট নমুনা; T আর α খোঁজা হয়নি। খরচ: student-এর state_dict ২৫.১ MiB, CPU-তে এক এক করে প্রায় ০.৫ ms আর পুরো test ০.১ সেকেন্ড; teacher-এর ৬২৭ MiB, ২৪.৬ ms আর ২৩.৫ সেকেন্ড (একই CPU, একই নিয়মে মাপা)। মান প্রায় ২ পয়েন্ট কম, গতি কয়েক দশ গুণ বেশি; এই বিনিময় গ্রহণযোগ্য কি না, তা ঠিক করে কাজের লক্ষ্য।

৮ · Pruning — magnitude, structured বনাম unstructured

বাগানের মালি গাছের সব ডাল কাটেন না; শুকনো আর ছোট ডাল ছাঁটেন, গাছ টিকে থাকে। Magnitude pruning-এর ধারণা একই: যে ওজনের পরম মান সবচেয়ে ছোট, তার প্রভাবও সম্ভবত ছোট, তাই সেটি শূন্য করা। শূন্যের অনুপাতকে বলে sparsity. যেকোনো জায়গার আলাদা আলাদা ওজন শূন্য করা হলো unstructured; পুরো neuron, attention head বা layer কেটে ফেলা structured. প্রথমটিতে কম ক্ষতিতে বেশি শূন্য করা যায়; দ্বিতীয়টিতে matrix সত্যিই ছোট হয়। একটি layer-এ তিনটি পদ্ধতি পাশাপাশি, retraining ছাড়া:

import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
d_in, d_out = 128, 64
W = rng.standard_t(df=4, size=(d_out, d_in)) * 0.05  # প্রশিক্ষিত ওজনের মতো: লম্বা লেজ
X = rng.normal(0, 1, (500, d_in))
Y = X @ W.T

def out_err(Wp):
    return np.linalg.norm(X @ Wp.T - Y) / np.linalg.norm(Y)

sp = np.array([0, 0.2, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95])
mag, rnd, row = [], [], []
for s in sp:
    thr = np.quantile(np.abs(W), s)            # magnitude: ছোট |w| শূন্য
    mag.append(out_err(np.where(np.abs(W) >= thr, W, 0) if s else W))
    keep = rng.random(W.shape) >= s            # তুলনা: এলোমেলো ওজন শূন্য
    rnd.append(out_err(W * keep))
    k = int(round(s * d_out))                  # structured: পুরো সারি (neuron) বাদ
    Wr = W.copy(); Wr[np.argsort(np.linalg.norm(W, axis=1))[:k]] = 0
    row.append(out_err(Wr))
print(" sparsity │ magnitude │ random │ সারি বাদ")
for s, m, r, w in zip(sp, mag, rnd, row):
    print(f"   {s:4.0%}   │  {m:.4f}   │ {r:.4f} │ {w:.4f}")
nz = np.abs(W) >= np.quantile(np.abs(W), 0.9)
print("90%-এ বাকি ওজন:", int(nz.sum()), "of", W.size,
      "│ dense গুণে তবু গুণ:", d_out * d_in, "টি")

fig, ax = plt.subplots(figsize=(5, 3))
ax.plot(sp * 100, mag, "o-", label="magnitude (unstructured)")
ax.plot(sp * 100, rnd, "s--", label="random")
ax.plot(sp * 100, row, "^:", label="whole rows (structured)")
ax.set_xlabel("sparsity (%)"); ax.set_ylabel("relative output error")
ax.set_title("Pruning one layer, no retraining"); ax.legend(fontsize=8)
plt.show()

আউটপুট (পাতার Python-এ চালানো):

 sparsity │ magnitude │ random │ সারি বাদ
     0%   │  0.0000   │ 0.0000 │ 0.0000
    20%   │  0.0498   │ 0.4261 │ 0.3822
    40%   │  0.1423   │ 0.6294 │ 0.5634
    50%   │  0.2043   │ 0.7102 │ 0.6350
    60%   │  0.2801   │ 0.7613 │ 0.7020
    70%   │  0.3720   │ 0.8202 │ 0.7770
    80%   │  0.4830   │ 0.8852 │ 0.8425
    90%   │  0.6374   │ 0.9433 │ 0.9221
    95%   │  0.7470   │ 0.9759 │ 0.9571
90%-এ বাকি ওজন: 820 of 8192 │ dense গুণে তবু গুণ: 8192 টি

৫০% sparsity-তে magnitude-এ output-এর ত্রুটি ০.২০, এলোমেলো বাছাইয়ে ০.৭১: ছোট ওজন সত্যিই কম গুরুত্বের, কারণ এই ওজনের বণ্টনের লেজ লম্বা, অল্প কিছু বড় ওজনেই বেশির ভাগ “শক্তি”। পুরো সারি বাদ দিলে ২০%-এই ত্রুটি ০.৩৮: একটি neuron-এর সব ওজন একসঙ্গে যায়, বড়গুলোও। তবু ৯০%-এ ৮,১৯২টির মধ্যে মাত্র ৮২০টি বাকি থাকলেও dense গুণে ৮,১৯২টি গুণই হয়, কারণ matrix-এর আকার বদলায়নি।

সত্যিকারের run, Bangla BERT teacher-এ. Encoder-এর সব Linear ওজনে (৮,৪৯,৩৪,৬৫৬টি) global magnitude pruning, কোনো retraining ছাড়া, আর তুলনায় FFN-এর neuron (intermediate layer-এর সারি) কাটা। Test macro-F1, GPU-তে মাপা:

কী শূন্যEncoder Linear-এর শূন্যMacro-F1
কিছু না০%০.৯৪২৫
Magnitude, unstructured২০% / ৪০% / ৫০%০.৯৪৩৬ / ০.৯৪০৯ / ০.৯৪২৮
Magnitude, unstructured৬০% / ৭০%০.৯৩৭২ / ০.৯০৮৯
Magnitude, unstructured৮০% / ৯০%০.৭২০৮ / ০.২২৮৪
FFN neuron ২৫% / ৫০% / ৭৫%৮.৩% / ১৬.৭% / ২৫%০.৯৩৩৬ / ০.৯১৭৬ / ০.৮৯৮৭

তারপর CPU-তে সময়, অংশ ৪-এর একই নিয়মে: ৯০% শূন্য করা model (আকার একই), আর FFN-এর অর্ধেক neuron সত্যিই কেটে ছোট Linear বানানো model (৩০৭২ থেকে ১৫৩৬ মাপ)। সত্যিকারের run:

Modelআকার (MiB)batch ১ (ms)পুরো test (s)Macro-F1
Dense fp32৬২৭.২২৪.৬২৩.৫০.৯৪২৫
৯০% unstructured৬২৭.২২৫.৩২৪.০০.২২৮৪
FFN ৫০% কাটা (structured)৫১৯.১১৫.২১৭.৭০.৯১৭৬

মান: retraining ছাড়াই ৫০% পর্যন্ত শূন্য করলে মান কার্যত একই (ছোট ওঠানামা মাপের সীমার মধ্যে), ৭০%-এ পড়তে শুরু করে, ৯০%-এ ভেঙে পড়ে: ০.২২৮৪ মানে প্রায় সব মন্তব্য এক শ্রেণিতে। বাস্তবে pruning-এর পর কয়েক epoch fine-tuning দিয়ে মান ফেরানো হয়, বা ধাপে ধাপে বাড়ানো হয়; সেটি এখানে মাপা হয়নি। গতি, সৎভাবে: ৯০% শূন্যের model-এ সময় dense-এর সমান (২৪.৬ বনাম ২৫.৩ ms), আকারও এক, কারণ শূন্যও একটি ৪ byte-এর সংখ্যা আর তাকেও গুণ করা হয়। FFN-এর অর্ধেক neuron সত্যিই কাটলে matrix ছোট, তাই batch ১-এ প্রায় ৩৮% দ্রুত, যদিও encoder-এর Linear ওজনের মাত্র এক-তৃতীয়াংশ কমেছে; মান ০.৯১৭৬, retraining ছাড়া। Attention আর embedding অপরিবর্তিত, তাই আকার কমেছে মাত্র ১৭%.

৯ · কোনটি কখন — তিন কৌশলের তুলনা

তিনটি কৌশল একে অপরের বিকল্প নয়; প্রায়ই একসঙ্গে লাগে। আমাদের মাপা ফলের আলোকে একটি সংক্ষিপ্ত ছক:

কৌশলTraining লাগে?কী কমেসাবধান
PTQ (int8, 4-bit)না, বা শুধু calibrationআকার; প্রায়ই সময়, হার্ডওয়্যার সমর্থন থাকলেOutlier, খুব কম bit, ছোট model; kernel না থাকলে ধীরও হতে পারে
QATহ্যাঁPTQ-এর মতো, কম bit-এ মান ভালোTraining-এর খরচ আর জটিলতা
Distillationহ্যাঁ, পুরো studentআকার আর সময়, যত ছোট student ততTeacher-এর মুখস্থ, transfer ডেটা, hard-label baseline
Unstructured pruningসাধারণত পরে fine-tuneঅশূন্য সংখ্যা; গতি শুধু বিশেষ kernel-এশূন্য ≠ গতি; sparse ফরম্যাটে না রাখলে আকারও একই
Structured pruningপ্রায় সবসময় পরে fine-tuneআকার আর সময়, সরাসরিমান বেশি পড়ে; কোন অংশ কাটবেন তা মাপা দরকার

ছোট করা model-কে নতুন model হিসেবে মাপুন: একই held-out সেট, আগে-পরে জোড়া তুলনা, বদলানো উত্তরের সংখ্যা, আর সময় আসল যন্ত্রে। ছোট করা model চালু করার কাজ (FastAPI, batch scoring, ONNX) মেশিন লার্নিং কোর্সের পর্ব ৬৮-এ; একই model ONNX Runtime-এ চালিয়ে আরও গতি পাওয়া যেতে পারে, তা এখানে মাপা হয়নি। এটি এক জোড়া মাপের ব্যাপার নয়: MLOps নিয়ে পরের একটি কোর্সে model-এর version আর নজরদারি আরও বিস্তারে আসবে।

আরও গভীরে: Hinton, Vinyals ও Dean-এর প্রবন্ধ (arXiv ১৫০৩.০২৫৩১) T² গুণকের যুক্তি আর বড় T-এ logit মেলানোর (Buciluǎ-র পদ্ধতি) সঙ্গে সম্পর্ক দেখায়। LLM.int8() (arXiv ২২০৮.০৭৩৩৯) outlier feature-এর আবির্ভাব আর mixed-precision decomposition; GPTQ (arXiv ২২১০.১৭৩২৩) Optimal Brain Surgeon-এর ধারণা থেকে layer-ভিত্তিক সংশোধন; AWQ (arXiv ২৩০৬.০০৯৭৮) activation দেখে গুরুত্বপূর্ণ channel রক্ষা। SmoothQuant-এর মতো কাজ activation-এর outlier-কে ওজনের দিকে সরিয়ে দুটিকেই int8-এ নেওয়ার প্রস্তাব দেয়। প্রতিটির দাবি তাদের নিজের model আর ইংরেজি benchmark-এ মাপা; বাংলায় যাচাই আলাদা।

পরীক্ষা ও ইন্টারভিউতে:

  • পরিসর [−১, ৩], uint8 asymmetric: s আর z? s = ৪/২৫৫ ≈ ০.০১৫৭, z = round(১/০.০১৫৭) = ৬৪।
  • ৭০০ কোটি parameter: fp16 আর int4-এ ওজন? ১৪ GB আর ৩.৫ GB, scale বাদে।
  • Per-channel কেন ভালো? প্রতিটি channel-এর নিজের scale, তাই একটি বড় channel বাকিদের শূন্যে ঠেলে না।
  • KD-তে T² কেন? Soft target-এর gradient ~1/T² হারে কমে; T² তা পুষিয়ে মাপ স্থির রাখে।
  • ৯০% unstructured sparsity মানে কি ১০ গুণ দ্রুত? না; dense kernel-এ শূন্যও গুণ হয়। Structured বা বিশেষ sparse kernel লাগে।

১০ · AI-এর যুগে — ছোট করার কোড আর দাবি যাচাই

Quantization আর distillation-এর কোড AI assistant দ্রুত লেখে, কিন্তু এই ক্ষেত্রের API খুব দ্রুত বদলায়: আমাদের run-এই torch.ao.quantization নিজে deprecation-এর সতর্কবার্তা দিয়েছে, আর bitsandbytes-এর argument transformers-এর version-ভেদে আলাদা।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
পুরনো argument (load_in_8bit=True সরাসরি from_pretrained-এ)নতুন version-এ BitsAndBytesConfig লাগেনিজের version-এর নথি পড়া
KD-তে F.kl_div(student_probs, teacher_probs)Input হতে হবে log-probability; T² বাদছোট উদাহরণে হাতে হিসাবের সঙ্গে মেলানো
Quantize-এর পরে model.eval() বা no_grad ভুলে যাওয়াDropout চলে, সময় আর ফল দুটিই ভুলদুবার চালিয়ে output একই কি না দেখা
“Pruning ৯০% = ১০ গুণ দ্রুত” লেখাDense kernel-এ শূন্যও গুণ হয়Warm-up-এর পর সময় মাপা
একটি run-এ “মান একই” দাবিছোট পার্থক্য seed-এর ওঠানামা হতে পারেকয়েকটি seed, জোড়া তুলনা, বদলানো উত্তরের সংখ্যা
বানানো benchmark বা “৯৯% মান” সংখ্যাউৎসহীন দাবিপ্রবন্ধ বা model card-এ খোঁজা, নিজে মাপা

কোড চাওয়ার সময় যাচাইও চেয়ে নিন:

আমি PyTorch আর transformers দিয়ে (version লিখে দিচ্ছি) একটি বাংলা classifier ছোট করতে চাই। ১. int8 dynamic quantization আর bitsandbytes 4-bit লোডের কোড দাও; প্রতিটি argument আমার version-এর নথিতে আছে কি না চিহ্নিত করো। ২. আগে-পরে আকার, warm-up-এর পরে সময় আর held-out test-এর macro-F1 মাপার কোড দাও। ৩. knowledge distillation-এর loss লেখো: log_softmax, T² গুণ আর reduction ঠিক রেখে; একটি ছোট উদাহরণে হাতে হিসাব দেখাও। ৪. একই student-কে শুধু hard label-এ শেখানোর baseline রাখো, আর কয়েকটি seed-এ চালাও। ৫. কোনো গতি, মান বা benchmark-এর সংখ্যা বানিয়ে লিখবে না; নিশ্চিত না হলে বলো।

AI-এর লেখা compression কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১৩২ bit থেকে ৪ bit-এ নামালে তথ্যের আট ভাগের সাত ভাগ ফেলে দেওয়া হয়। তবু বড় language model প্রায় একই রকম কাজ করে কেন? আর কোথায় এই কৌশল ভাঙে?

প্রথম কারণ, training-এ যে নির্ভুলতা লাগে, inference-এ তা লাগে না। Training-এ ছোট ছোট gradient হাজার হাজার ধাপে জমা হয়; সেখানে rounding-এর ত্রুটি জমে বড় হতে পারে, তাই mixed precision-এও fp32 master কপি রাখা হয় (পর্ব ১২)। Inference-এ ওজন স্থির, শুধু একবার গুণ-যোগ। প্রতিটি ওজনের ত্রুটি সর্বোচ্চ s/2, আর এলোমেলো দিকের অনেক ছোট ত্রুটি একটি dot product-এ যোগ হলে অনেকটা কাটাকাটি হয়: মোট ত্রুটি ত্রুটির সংখ্যার বর্গমূলের হারে বাড়ে, সংখ্যার হারে নয়।

দ্বিতীয় কারণ, neural network-এর সিদ্ধান্ত সাধারণত সূক্ষ্ম মানের ওপর দাঁড়িয়ে থাকে না। Softmax-এর সবচেয়ে বড় logit যদি পরেরটির চেয়ে অনেক এগিয়ে থাকে, logit-এ সামান্য নড়াচড়া সিদ্ধান্ত বদলায় না। আমাদের CPU-র int8 run-এ ২,১৪৩টি test মন্তব্যের প্রায় সবগুলোতেই fp32-এর সঙ্গে একই সিদ্ধান্ত এসেছে, যদিও logit-এ পার্থক্য ছিল। তৃতীয়ত, বড় model-এ একই তথ্য অনেক ওজনে ছড়িয়ে থাকে, যা একধরনের বাড়তি নিরাপত্তা দেয়।

কোথায় ভাঙে? এক, outlier: অংশ ৩-এ দেখেছেন, একটি বড় মান পুরো scale নষ্ট করে। LLM.int8() প্রবন্ধের মূল আবিষ্কারই ছিল যে বড় model-এ এমন outlier feature নিয়মিত দেখা দেয়, আর সাধারণ int8 তখন মান নষ্ট করে। দুই, খুব কম bit: 3 বা 2 bit-এ সাধারণ round-to-nearest প্রায়ই ভেঙে পড়ে; GPTQ বা AWQ-এর মতো সতর্ক পদ্ধতি, বা quantization-aware training লাগে। তিন, ছোট model: বাড়তি নিরাপত্তা কম, তাই একই bit-এ ক্ষতি বেশি হওয়ার প্রবণতা দেখা গেছে; আমাদের ০.৫B model-এর perplexity-তে এর ইঙ্গিত মাপা আছে। চার, কিছু কাজ বেশি স্পর্শকাতর: দীর্ঘ যুক্তি, গণিত, কম-সম্পদের ভাষা। গড় perplexity প্রায় একই থাকলেও এগুলোতে আলাদা মাপ লাগে, কারণ গড় অনেক কিছু লুকায়।

মূল উপলব্ধি: Quantization কাজ করে কারণ inference-এ ছোট এলোমেলো ত্রুটি কাটাকাটি হয় আর সিদ্ধান্তের ফাঁক বড় থাকে। Outlier, খুব কম bit, ছোট model আর স্পর্শকাতর কাজে এই ধারণা দুর্বল হয়, তাই প্রতিবার নিজের কাজের ডেটায় মাপুন।

প্র ০২Student কেন hard label-এর চেয়ে teacher-এর নরম probability থেকে বেশি শিখতে পারে? Hinton-এর “dark knowledge” ধারণার সীমা কোথায়?

Hinton, Vinyals ও Dean ২০১৫ সালের প্রবন্ধে একটি উদাহরণ দেন: MNIST-এ একটি ২-কে teacher হয়তো খুব ছোট probability দেয় “৩” আর আরও ছোট “৭”-কে। এই ছোট সংখ্যাগুলো বলে কোন অঙ্কগুলো দেখতে কাছাকাছি, যা one-hot label কখনো বলে না। প্রতিটি training উদাহরণ তখন একটি বিট নয়, পুরো একটি বণ্টনের তথ্য বহন করে। Hinton একটি বক্তৃতায় এর নাম দেন “dark knowledge”. Temperature এই ছোট সংখ্যাগুলোকে বড় করে দেখায়; T = ১-এ এগুলো এত ছোট যে loss-এ প্রায় কোনো ভূমিকা রাখে না।

আরেকটি ব্যাখ্যা regularization-এর দিক থেকে। Soft target-এর সঙ্গে মেলাতে গেলে student অতি আত্মবিশ্বাসী হতে পারে না; এটি label smoothing-এর মতো, কিন্তু প্রতিটি উদাহরণের জন্য আলাদা, teacher-এর জানা মিল অনুযায়ী। প্রবন্ধে তাঁরা দেখান, speech recognition-এ একটি ensemble-এর জ্ঞান একটি model-এ distill করে ensemble-এর প্রায় কাছাকাছি মান পাওয়া গেছে। পরে DistilBERT (Sanh ও সহকর্মীরা, ২০১৯) BERT-এর অর্ধেক layer নিয়ে, prediction-এর soft target ছাড়াও hidden state মেলানোর loss যোগ করে, প্রবন্ধের হিসাবে ৪০% ছোট আর ৬০% দ্রুত model-এ GLUE-তে ৯৭% মান ধরে রাখার কথা জানায়।

সীমা কোথায়? এক, teacher যদি training ডেটায় প্রায় নিখুঁত মুখস্থ করে ফেলে, তার train-set-এর output প্রায় one-hot, তখন soft target-এ নতুন তথ্য কম। আমাদের run-এ ঠিক এই ব্যাপারটি মাপা হয়েছে। সমাধান: আলাদা “transfer set” (label ছাড়া নতুন ডেটা), বা teacher-কে কম মুখস্থ করানো। দুই, দুই শ্রেণির কাজে বণ্টনে তথ্যই কম: একটি সংখ্যা জানলেই অন্যটি জানা। বহু শ্রেণি বা language model-এর পরের token-এর বণ্টনে soft target অনেক বেশি তথ্যপূর্ণ। তিন, পরে কিছু গবেষণা দেখিয়েছে student প্রায়ই teacher-কে পুরোপুরি নকল করতে পারে না, আর student-teacher-এর আকারের ফাঁক খুব বড় হলে মাঝারি “teacher assistant” সাহায্য করতে পারে; এগুলো নির্দিষ্ট পরীক্ষার ফল, সার্বজনীন নিয়ম নয়।

মূল উপলব্ধি: Soft target প্রতিটি উদাহরণে শ্রেণিগুলোর মিলের তথ্য দেয় আর regularizer-এর কাজ করে। লাভ নির্ভর করে teacher কতটা মুখস্থ করেছে, শ্রেণি কতগুলো আর ডেটা কতটা নতুন তার ওপর; তাই একই student hard label-এ শিখিয়ে তুলনা না করে লাভ দাবি করবেন না।

প্র ০৩Pruning-এ ৯০% ওজন শূন্য করা যায়, তবু গতি প্রায় বাড়ে না কেন? Lottery ticket hypothesis এই ছবিতে কোথায় বসে?

Pruning-এর ইতিহাস পুরনো। LeCun ও সহলেখকদের “Optimal Brain Damage” (১৯৮৯) loss-এর দ্বিতীয় derivative দিয়ে কোন ওজন সরালে ক্ষতি কম তা বেছেছিল। Han ও সহলেখকেরা ২০১৫ সালে দেখান, শুধু ছোট মাপের ওজন সরিয়ে আবার training দিলে AlexNet আর VGG-এর মতো network-এর অনেক ওজন বাদ দিয়েও তাঁদের পরীক্ষায় মান প্রায় অক্ষত থাকে; পরে “Deep Compression”-এ pruning, quantization আর Huffman coding একসঙ্গে জোড়েন।

কিন্তু শূন্য রাখা আর হিসাব বাদ দেওয়া এক জিনিস নয়। GPU আর CPU-র matrix গুণের library ঘন (dense) matrix-এর জন্য বানানো: শূন্যও একটি সংখ্যা, তাকেও গুণ করা হয়। এলোমেলো জায়গার শূন্য বাদ দিতে sparse ফরম্যাট লাগে, যেখানে প্রতিটি অশূন্য মানের সঙ্গে তার অবস্থানও রাখতে হয়, আর memory থেকে অনিয়মিত পড়া হয়। সাধারণত sparsity খুব বেশি না হলে এই খরচে লাভ মুছে যায়। আমাদের CPU run-এ ৯০% শূন্যের model-এর সময় dense-এর প্রায় সমান এসেছে। NVIDIA-র কিছু GPU-তে “২:৪” ধরনের semi-structured sparsity-র (প্রতি চারটির দুটি শূন্য) হার্ডওয়্যার সমর্থন আছে বলে NVIDIA-র নথিতে বলা হয়, কিন্তু সেটি নির্দিষ্ট নকশা আর নির্দিষ্ট হার্ডওয়্যারের কথা। পুরো neuron, head বা layer কেটে ফেলা (structured) সরাসরি ছোট matrix দেয়, তাই যেকোনো হার্ডওয়্যারে গতি বাড়ে, কিন্তু মান বেশি পড়ে।

Frankle ও Carbin-এর lottery ticket hypothesis (ICLR ২০১৯) একটি ভিন্ন প্রশ্ন তোলে: বড় network-এর ভেতরে কি শুরু থেকেই একটি ছোট sub-network আছে, যা একা শেখালেও একই মানে পৌঁছায়? তাঁরা দেখান, magnitude pruning-এ পাওয়া sparse নকশাটি তার আসল শুরুর ওজনে ফিরিয়ে শেখালে ছোট network-এ ভালো ফল আসে, আর নতুন এলোমেলো শুরুতে আসে না। পরে বড় network-এ এর জন্য শুরুতে নয়, training-এর একটু পরের ওজনে ফেরানো লেগেছে বলে লেখকেরাই জানান। এটি বোঝার জন্য গুরুত্বপূর্ণ, কিন্তু সরাসরি গতি দেয় না; টিকিট খুঁজতে বারবার পুরো training লাগে।

মূল উপলব্ধি: Sparsity আর গতি আলাদা জিনিস। গতি চাইলে হার্ডওয়্যার যে নকশা দ্রুত চালায় (structured বা ২:৪) সেটি বেছে নিন, আর সময় নিজের যন্ত্রে মাপুন; কত শতাংশ শূন্য, সেই সংখ্যা একা কিছু প্রমাণ করে না।

প্র ০৪বাজারঘর একটি বাংলা গ্রাহক-সেবা model ফোনে বা সস্তা server-এ চালাতে চায়। Quantization, distillation আর pruning কোন ক্রমে, কোন যাচাই দিয়ে প্রয়োগ করবেন?

প্রথম ধাপ লক্ষ্য লেখা: কত MB পর্যন্ত, প্রতি উত্তরে কত মিলিসেকেন্ড, কোন হার্ডওয়্যার (ফোনের CPU, GPU-হীন server), আর মান কতটা পড়া চলবে। লক্ষ্য না থাকলে “ছোট করা” কখনো শেষ হয় না। তারপর একটি স্থির মূল্যায়ন সেট আর baseline: আসল model-এর মান, সময় আর আকার, পর্ব ৫৮-এর নিয়মে confidence interval-সহ। বাংলা-বিশেষ প্রশ্ন (সংখ্যা, দাম, জেলার নাম, মিশ্র বাংলা-ইংরেজি) আলাদা ভাগে রাখুন, কারণ ছোট করার ক্ষতি প্রায়ই সেখানেই আগে দেখা দেয়।

সবচেয়ে সস্তা পদক্ষেপ আগে: post-training quantization. ওজন int8-এ, যদি চলে তবে 4-bit-এ; training লাগে না, কয়েক মিনিটের কাজ। আমাদের CPU run-এ int8-এ আকার আর সময় দুটিই কমেছে আর মান প্রায় একই ছিল। এটুকুতে লক্ষ্য পূরণ হলে থামুন। না হলে distillation: ছোট একটি student, teacher-এর soft target-এ, আর বাজারঘরের নিজের label ছাড়া প্রশ্নগুলো transfer set হিসেবে। Distillation-এর খরচ training-এর সমান, কিন্তু আকার কয়েক গুণ কমাতে পারে। Pruning রাখুন শেষে, আর structured রূপে (head বা FFN neuron কাটা), তারপর অল্প fine-tuning দিয়ে মান ফেরান; unstructured শূন্য ফোনে গতি দেবে এমন ধরে নেবেন না।

কৌশলগুলো জোড়া যায়, কিন্তু ক্রম গুরুত্বপূর্ণ। সাধারণত আগে আকার বদলানো (distillation বা structured pruning), তারপর quantization, কারণ quantization শেষে ছোট ত্রুটি যোগ করে আর তার পরে training করলে সব আবার করতে হয়। প্রতিটি ধাপের পর একই মূল্যায়ন সেট, একই seed, আর আগে-পরে সিদ্ধান্ত মেলানো (কতগুলো উত্তর বদলাল)। Regression তালিকা রাখুন: আগে ঠিক ছিল, এখন ভুল। আর মাপুন আসল যন্ত্রে, warm-up-এর পরে, batch আকার যা আসলে হবে তাই দিয়ে।

মূল উপলব্ধি: লক্ষ্য আর মূল্যায়ন সেট আগে, তারপর সস্তা থেকে দামি কৌশল: PTQ, তারপর distillation, শেষে structured pruning. প্রতিটি ধাপের পর একই মাপকাঠিতে মান, আকার আর সময় লিখে রাখুন।

প্র ০৫“এই 4-bit model মূল model-এর ৯৯% মান ধরে রাখে” — এমন দাবি কীভাবে পড়বেন? Perplexity আর benchmark-এর গড় কী লুকাতে পারে?

প্রথম প্রশ্ন: কোন মাপে ৯৯%? Perplexity, একটি benchmark-এর accuracy, নাকি কয়েকটির গড়? Perplexity সব token-এর গড় cross-entropy-র exp; সাধারণ token-এ ছোট উন্নতি বিরল কিন্তু জরুরি token-এর বড় ক্ষতিকে ঢেকে দিতে পারে। আমাদের run-এ ০.৫B model-এ 4-bit-এ perplexity কতটা বাড়ল, তা মাপা আছে; কিন্তু সেটি গল্পের ভাষার গড়, গ্রাহক-সেবার উত্তরের মান নয়। পর্ব ৫৮-এ দেখেছেন, একটি গড় সংখ্যা ছোট দলের ব্যর্থতা লুকাতে পারে।

দ্বিতীয় প্রশ্ন: কোন ডেটায় আর কোন ভাষায়? প্রকাশিত quantization প্রবন্ধগুলো সাধারণত ইংরেজি perplexity (যেমন WikiText বা C4) আর কিছু ইংরেজি benchmark-এ মাপে। বাংলার মতো ভাষা, যা model কম দেখেছে আর যার token বেশি টুকরো হয়, সেখানে ক্ষতি আলাদা হতে পারে; কয়েকটি গবেষণায় বহুভাষিক model-এ quantization-এর ক্ষতি ভাষাভেদে অসমান পাওয়ার কথা বলা হয়েছে, তবে এর মাত্রা model আর পদ্ধতির ওপর নির্ভর করে। তাই নিজের ভাষার held-out ডেটায় মাপা ছাড়া উপায় নেই। তৃতীয় প্রশ্ন: তুলনা কি একই প্রম্পট, একই decoding আর একই seed-এ? Generation-এ greedy-র এক token বদলালেই বাকি উত্তর আলাদা পথে যায়, যেমন আমাদের bf16 আর 4-bit-এর উত্তরে দেখা যায়।

সৎভাবে পড়ার নিয়ম: (১) মাপটি কী, আর আপনার কাজের সঙ্গে তার সম্পর্ক কী; (২) মূল আর ছোট model-এর জোড়া তুলনা একই উদাহরণে, paired bootstrap বা McNemar দিয়ে (মেশিন লার্নিং কোর্সের পর্ব ২২); (৩) উত্তর বদলানোর হার, শুধু গড় নয়; (৪) সবচেয়ে খারাপ দলের মান; (৫) হার্ডওয়্যার আর library-র version, কারণ একই “4-bit” নামের পেছনে ভিন্ন পদ্ধতি থাকে (NF4, GPTQ, AWQ বা GGUF-এর বিভিন্ন ধরন)।

মূল উপলব্ধি: “৯৯% মান” একটি নির্দিষ্ট মাপ, ডেটা আর পদ্ধতির দাবি। নিজের ভাষা আর কাজের held-out ডেটায়, জোড়া তুলনা আর বদলানো উত্তরের তালিকা দিয়ে যাচাই করে তবেই বিশ্বাস করুন।

অনুশীলন

খাতা-কলমে করুন, তারপর উত্তর মেলান। পরীক্ষা আর ইন্টারভিউতে এই ধরনের হিসাব কাজে লাগে।

  1. হিসাব করুন: একটি activation tensor-এর মান −১.০ থেকে ৩.০-এর মধ্যে। (ক) uint8 (০–২৫৫) asymmetric quantization-এ scale আর zero point কত? (খ) x = ১.৫ কোন পূর্ণসংখ্যায় যায়, আর ফিরিয়ে আনলে কত, ত্রুটি কত? (গ) ৭০০ কোটি parameter-এর একটি model-এর ওজন bf16 থেকে int4-এ নিলে memory কত থেকে কততে নামে (scale বাদে)?

    (ক) s = (৩.০ − (−১.০)) / ২৫৫ = ৪ / ২৫৫ ≈ ০.০১৫৬৯; z = round(০ − (−১.০)/০.০১৫৬৯) = round(৬৩.৭৫) = ৬৪. (খ) ১.৫ / ০.০১৫৬৯ ≈ ৯৫.৬২, round করে ৯৬; q = ৯৬ + ৬৪ = ১৬০. ফেরত: (১৬০ − ৬৪) × ০.০১৫৬৯ ≈ ১.৫০৫৯; ত্রুটি প্রায় ০.০০৫৯, যা s/2 ≈ ০.০০৭৮-এর কম। (গ) bf16: ২ byte × ৭ × ১০৯ = ১৪ GB; int4: ০.৫ byte × ৭ × ১০৯ = ৩.৫ GB. ৬৪-এর block-এ একটি ১৬-bit scale যোগ করলে প্রতি parameter-এ আরও ০.২৫ bit, মোট প্রায় ৩.৭২ GB.

  2. আউটপুট অনুমান করুন: কোড না চালিয়ে চারটি লাইন লিখুন।
    import numpy as np
    x = np.array([-0.4, 0.0, 0.26, 1.0])
    s = np.abs(x).max() / 127
    q = np.round(x / s).astype(int)
    print(q)
    print(np.round(q * s, 4))
    w = np.array([0.5, -0.05, 0.3, -0.8, 0.02, 0.1])
    print(np.where(np.abs(w) >= np.quantile(np.abs(w), 0.5), w, 0))
    print(round(4 * 0.25, 2), "vs", round(0.25 / 4, 4))
    [-51   0  33 127]
    [-0.4016  0.      0.2598  1.    ]
    [ 0.5  0.   0.3 -0.8  0.   0. ]
    1.0 vs 0.0625

    Symmetric: s = ১/১২৭। −০.৪ × ১২৭ = −৫০.৮ → −৫১; ০.২৬ × ১২৭ = ৩৩.০২ → ৩৩; ১.০ → ১২৭। ফেরত −৫১/১২৭ ≈ −০.৪০১৬, ৩৩/১২৭ ≈ ০.২৫৯৮। Pruning: |w|-এর median (০.৩ আর ০.১-এর গড়) = ০.২; ০.২-এর নিচের তিনটি (−০.০৫, ০.০২, ০.১) শূন্য। শেষ লাইন KD-র শিক্ষা: T = ২-এ gradient-এর মাপ ০.২৫ হলে T² দিয়ে গুণ করলে ১.০, আর ভুল করে T² দিয়ে ভাগ করলে মাত্র ০.০৬২৫।

  3. নকশা সমালোচনা করুন: একটি দলের পরিকল্পনা: “আমাদের বাংলা classifier-এর সব layer, embedding-সহ, per-tensor int4-এ নেব, কোনো calibration ছাড়া। তারপর ৯০% unstructured pruning করব যাতে ফোনে দশ গুণ দ্রুত হয়। মান যাচাই করব training set-এর accuracy দিয়ে। Distillation-এর teacher হবে এমন একটি model, যা training set-এ ১০০% accuracy পায়, T = ১ রেখে।” অন্তত পাঁচটি সমস্যা আর সমাধান বলুন।
    • Per-tensor int4: outlier একটি scale নষ্ট করে, ১৬টি ঘরে বাকি মান শূন্যে জড়ো হয়। সমাধান: per-channel বা ৬৪-এর group-wise scale, outlier আলাদা; শুরুতে int8.
    • Embedding আর classifier-ও int4: এই অংশগুলো প্রায়ই বেশি স্পর্শকাতর। সমাধান: সেগুলো ১৬-bit বা int8-এ রাখা, আলাদা করে মাপা।
    • ৯০% unstructured থেকে দশ গুণ গতির আশা: dense হার্ডওয়্যারে শূন্যও গুণ হয়। সমাধান: structured pruning, আর ফোনেই সময় মাপা।
    • Training set-এ মাপা: ক্ষতি লুকায়। সমাধান: আলাদা held-out test, আগে-পরে জোড়া তুলনা, বদলানো উত্তরের সংখ্যা।
    • T = ১ আর মুখস্থ করা teacher: train-এ তার output প্রায় one-hot, soft target-এ নতুন তথ্য নেই। সমাধান: T = ২–৪, T² গুণক, আর label ছাড়া আলাদা transfer ডেটা।
    • ক্রম: quantize-এর পরে pruning আর distillation করলে ত্রুটি জমে। সমাধান: আগে আকার ঠিক, শেষে quantization.
  4. কোড লিখুন: একটি function kd_loss(zs, zt, y, T, alpha) লিখুন, যা একটি উদাহরণের logit (NumPy array) নিয়ে α·CE + (1 − α)·T²·KL ফেরত দেয়। zt = [৩, ১, −১], zs = [২, ১.৫, −০.৫], y = ০, T = ২, α = ০.৫-এ ০.৩৯৫২ আসে কি না যাচাই করুন। তারপর যাচাই করুন zs = zt হলে KD অংশ ঠিক ০।
    import numpy as np
    def softmax(z, T=1.0):
        e = np.exp((z - z.max()) / T)
        return e / e.sum()
    
    def kd_loss(zs, zt, y, T, alpha):
        p, q = softmax(zt, T), softmax(zs, T)
        kd = T**2 * np.sum(p * (np.log(p) - np.log(q)))
        ce = -np.log(softmax(zs)[y])
        return alpha * ce + (1 - alpha) * kd
    
    zt, zs = np.array([3., 1, -1]), np.array([2., 1.5, -0.5])
    assert abs(kd_loss(zs, zt, 0, 2, 0.5) - 0.3952) < 1e-4
    assert abs(kd_loss(zt, zt, 0, 2, 0.0)) < 1e-12

    সর্বোচ্চ মান বিয়োগ করে exp নেওয়া হয়েছে, যাতে বড় logit-এ overflow না হয়। ভুলের সাধারণ জায়গা: KL-এর দিক উল্টো (KL(q ‖ p)), T² ভুলে যাওয়া, বা CE-তেও T দিয়ে ভাগ করা। PyTorch-এ F.kl_div-এ input log-probability আর reduction="batchmean" লাগে (পর্ব ১৩)।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি শেখানো model ছোট করে চালু করার নিরাপদ ধাপগুলো সাজান (▲▼ চেপে সরান)

  1. আকার, সময় আর মানের লক্ষ্য লিখে একটি স্থির মূল্যায়ন সেটে আসল model মাপা
  2. দরকার হলে distillation বা structured pruning দিয়ে model-এর গঠন ছোট করা
  3. ছোট model-কে post-training quantization দিয়ে int8 বা 4-bit-এ নেওয়া
  4. একই সেটে মান, আগে-পরে বদলানো উত্তর আর আসল যন্ত্রে সময় মাপা
  5. লক্ষ্য পূরণ হলে version আর মাপসহ চালু করা, নইলে আগের ধাপে ফেরা

একটি tensor-এর মান ০ থেকে ৫.১-এর মধ্যে। uint8 (০–২৫৫) asymmetric quantization-এ scale আর zero point কত?

  • s ≈ ০.০৪০২, z = ০
  • s = ০.০২, z = ১২৮
  • s = ০.০২, z = ০
  • s = ৫০, z = ০

৫০ কোটি (5 × 108) parameter-এর একটি model fp32 থেকে int8-এ নিলে, scale-এর খরচ বাদে, ওজনের memory কত কমে?

  • ২ GB থেকে ১ GB
  • ২ GB থেকে ০.৫ GB
  • ৪ GB থেকে ১ GB
  • ২ GB থেকে ০.০৬২৫ GB

একটি activation matrix-এ একটি feature কলামের মান বাকিদের চেয়ে প্রায় ৬০ গুণ বড়। Per-tensor int8 quantization-এ কী ঘটার সম্ভাবনা সবচেয়ে বেশি?

  • শুধু outlier কলামের মান ভুল হয়, বাকিগুলো নিখুঁত থাকে
  • সব মান int8-এর সীমা ছাড়িয়ে overflow করে
  • ত্রুটি per-channel-এর চেয়ে কম হয়, কারণ scale একটিই
  • সাধারণ কলামগুলোর বেশির ভাগ মান round হয়ে ০-তে নেমে যায়

Knowledge distillation-এর loss-এ KL অংশকে T² দিয়ে গুণ করা হয় কেন?

  • বড় T-এ KL অংশের gradient প্রায় 1/T² হারে ছোট হয়, সেটি পুষিয়ে দিতে
  • যাতে KL ঋণাত্মক না হয়
  • যাতে teacher-এর বণ্টন আরও নরম হয়
  • যাতে hard label-এর cross-entropy-ও temperature পায়

একটি BERT classifier-এর encoder-এর Linear ওজনের ৯০% magnitude pruning দিয়ে শূন্য করা হলো, কিন্তু matrix-এর আকার একই রাখা হলো। সাধারণ CPU-তে PyTorch-এর dense গুণে কী আশা করা যায়?

  • সময় প্রায় দশ ভাগের এক ভাগে নামবে
  • Memory প্রায় দশ ভাগের এক ভাগে নামবে
  • সময় প্রায় একই থাকবে
  • সময় অনেক বাড়বে, কারণ শূন্য সামলাতে বাড়তি কাজ

PyTorch-এর dynamic int8 quantization (CPU) একটি BERT classifier-এ কী করে?

  • Calibration ডেটা দিয়ে activation-এর scale আগে থেকে ঠিক করে
  • Linear-এর ওজন আগে int8-এ রাখে, activation প্রতিটি batch-এ চলার সময় quantize করে
  • Training-এর সময় rounding নকল করে model আবার শেখায়
  • Embedding-সহ সব ওজন int8-এ নেয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Memory = parameter × byte: fp32 ৪, bf16 ২, int8 ১, int4 ০.৫, সঙ্গে scale; embedding প্রায়ই quantize হয় না।
  2. q = round(x/s) + z, x̂ = (q − z)·s; ত্রুটি ≤ s/2. Asymmetric একদিকে হেলা মানে ভালো, symmetric শূন্যকেন্দ্রিক ওজনে সহজ।
  3. Outlier scale নষ্ট করে: per-channel, group-wise বা outlier আলাদা (LLM.int8()); GPTQ, AWQ আর GGUF একই সমস্যার ভিন্ন উত্তর।
  4. KD loss = α·CE + (1 − α)·T²·KL(pT ‖ qT); লাভ দাবি করার আগে একই student hard label-এ শিখিয়ে তুলনা করুন।
  5. Unstructured শূন্য ≠ গতি; structured কাটা ছোট matrix দেয়। প্রতিটি কৌশলের পর একই সেটে মান, আকার আর সময় মাপুন।

পরের পর্বে: পর্ব ৬০-এ নতুন মডিউল, prompt engineering: একটি ভালো prompt-এর গঠন, স্পষ্টতা, প্রসঙ্গ আর উদ্দেশ্য; model না বদলে শুধু প্রশ্ন বদলে উত্তর কতটা বদলায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান