এই পর্বে যা শিখবেন
- Full fine-tuning-এ ওজন, gradient আর optimizer state-এর memory হাতে হিসাব করা
- LoRA-র সূত্র ΔW = BA, rank, alpha, scaling আর B = 0 শুরুর কারণ
- যেকোনো model-এর আকার থেকে LoRA-র parameter হাতে গোনা, আর target modules বাছাই
- Merge: adapter ওজনে মিশিয়ে একই output যাচাই, আর কখন merge করবেন না
- QLoRA-র তিনটি কৌশল, আর adapter, prefix tuning, prompt tuning ও IA3-এর জায়গা
পর্ব ৪৮ · BERT পরিবার-এ আপনি Bangla BERT-এর সব ১৬.৪ কোটি parameter Sazzed-এর বাংলা YouTube মন্তব্যে শিখিয়েছেন। সেটিই full fine-tuning. পর্ব ৫৩ · Hugging Face ecosystem-এ Hub আর Auto class দেখেছেন। Trainer দিয়ে সাধারণ fine-tuning-এর রেসিপি পর্ব ৫৪ · Transformer fine-tuning — classification, NER আর বাংলা ডেটা-য় আছে। এই পর্বের প্রশ্ন অন্য: সব parameter না ছুঁয়ে কি একই কাজ হয়? Desktop-এর run লেখার সময়ে (২০২৬ সালের নভেম্বর) এই version-এ: Python 3.12, torch 2.15.0, transformers 5.22.2, peft 0.21.2, bitsandbytes 0.50.2. GPU-র run GPU-তে, বাকিটা CPU-তে, যেখানে যেটি তা লেখা আছে।
১ · Full fine-tuning-এর খরচ — memory হাতে হিসাব
একটি রেডিমেড শার্ট কিনলেন, হাতা একটু লম্বা। দর্জি পুরো শার্ট খুলে নতুন করে সেলাই করেন না; হাতায় দুটি সেলাই দেন। Full fine-tuning হলো পুরো শার্ট নতুন করে সেলাই করা। ফল ভালো হতে পারে, কিন্তু খরচ পুরো শার্টের।
খরচটি কোথায়, হিসাব করি। Adam আর AdamW প্রতিটি parameter-এর জন্য দুটি বাড়তি সংখ্যা মনে রাখে: gradient-এর চলমান গড় m (first moment) আর বর্গের চলমান গড় v (second moment). এর সূত্র মেশিন লার্নিং কোর্সের পর্ব ১৪-এ derive করা; PyTorch-এ ব্যবহার পর্ব ১৫ · Optimizer আর schedule-এ। সব fp32 হলে প্রতি parameter-এ:
ওজন ৪ + gradient ৪ + m ৪ + v ৪ = ১৬ byte
Mixed precision-এ ওজন আর gradient bf16-এ (২ + ২ byte) রাখলেও সাধারণত একটি fp32 “master” কপি (৪ byte) থাকে, ফলে মোট আবার ১৬ byte. এর ওপর আসে activation memory: backward-এর জন্য forward-এর মাঝের ফল জমা রাখা, যা batch আর sequence দৈর্ঘ্যের ওপর নির্ভর করে (পর্ব ১২ · GPU performance). নিচের stepper-এ আমাদের model, sagorsarker/bangla-bert-base (MIT লাইসেন্স), classifier head-সহ ১৬,৪৩,৯৮,০৮২টি parameter.
| কী | |
| হিসাব | |
| MiB |
- ওজন. ১৬,৪৩,৯৮,০৮২টি fp32 সংখ্যা, প্রতিটি ৪ byte: ৬৫,৭৫,৯২,৩২৮ byte, অর্থাৎ ২২০ দিয়ে ভাগ করে ৬২৭.১ MiB.
- Gradient. প্রতিটি শেখার parameter-এর একটি gradient, একই আকার: আরও ৬২৭.১ MiB.
- Adam-এর m আর v. দুটি, তাই ৮ byte করে: ১,২৫৪.৩ MiB. Optimizer-এর স্মৃতি একাই ওজনের দ্বিগুণ।
- Full-এর মোট. ১৬ byte × parameter = ২,৫০৮.৫ MiB, activation বাদ দিয়েই। ৪৯ কোটির Qwen2.5-0.5B-তে একই হিসাবে প্রায় ৭.৯ GB.
- LoRA-তে ওজন. Base-এর ওজন memory-তে থাকেই (৬২৭.১), সঙ্গে LoRA আর head-এর ২,৯৬,৪৫০টি সংখ্যা: মাত্র ১.১ MiB বাড়তি।
- LoRA-তে gradient আর Adam. শুধু ২,৯৬,৪৫০টির জন্য: ৩.৪ MiB. Frozen ওজনের gradient বা moment লাগে না।
- মাপা peak. অংশ ৭-এর run-এ full-এর peak ৩,৯৬৯ MiB, LoRA r = ৮-এর ১,৭৬৭ MiB. পার্থক্য ২,২০২ MiB, হাতের হিসাবের ২,৫০৮.৫ − ৬৩১.৭ = ১,৮৭৬.৮ MiB-এর কাছাকাছি; বাকিটা দুটিতেই activation আর কাজের জায়গা।
দ্বিতীয় খরচ জমা রাখার। বাজারঘর যদি পাঁচটি কাজে (রিভিউয়ের মনোভাব, অভিযোগের ধরন, ডেলিভারির প্রশ্ন…) একই base model আলাদা করে full fine-tune করে, তবে পাঁচটি পুরো কপি, প্রতিটি শত শত MB. LoRA-তে একটি base আর পাঁচটি ছোট adapter ফাইল।
২ · LoRA-র মূল ধারণা — ΔW = BA
Fine-tuning শেষে একটি ওজন matrix W0 থেকে হয় W0 + ΔW. Full fine-tuning ΔW-এর প্রতিটি ঘর আলাদা করে শেখে। LoRA (Low-Rank Adaptation; Hu ও সহলেখক, arXiv ২০২১, ICLR ২০২২) একটি অনুমান করে: নতুন কাজে মানিয়ে নেওয়ার পরিবর্তনটির “আসল” মাত্রা কম, অর্থাৎ ΔW-এর rank কম। তাহলে ΔW-কে দুটি সরু matrix-এর গুণফল হিসেবে লেখা যায়:
h = W0x + (α / r) · B A x, B ∈ ℝd×r, A ∈ ℝr×k, r ≪ min(d, k)
এখানে x হলো layer-এর input (দৈর্ঘ্য k), h output (দৈর্ঘ্য d), W0 হলো pretrained ওজন, যা frozen থাকে, মানে gradient পায় না। A প্রথমে x-কে r মাত্রায় চাপে, B আবার d মাত্রায় ছড়ায়। r হলো LoRA rank, α (lora_alpha) একটি স্থির গুণক। BA-র rank কখনো r-এর বেশি হতে পারে না, কারণ মাঝের পথ মাত্র r চওড়া।
সংখ্যায়: d = k = ৭৬৮ হলে ΔW-তে ৫,৮৯,৮২৪টি ঘর। r = ৮-এ A আর B মিলিয়ে ৮ × (৭৬৮ + ৭৬৮) = ১২,২৮৮টি, প্রায় ২%. অনুমানটি কেন যুক্তিসঙ্গত? Aghajanyan ও সহলেখকেরা (২০২০) দেখান, বড় pretrained model অনেক কাজ খুব কম মাত্রার একটি উপস্থানে (intrinsic dimension) শিখতে পারে; LoRA-র লেখকেরা এই ফলকেই অনুপ্রেরণা বলেছেন। তবে এটি পর্যবেক্ষণ, প্রমাণ নয়: কাজ base model-এর জ্ঞান থেকে যত দূরে (নতুন ভাষা, নতুন ধরনের ডেটা), ছোট r তত কম যথেষ্ট হতে পারে।
উপমা: শিক্ষক পুরো বই নতুন করে না লিখে একটি পাতলা নোট-খাতা দেন: “অধ্যায় ৩-এর এই ধারণাটি এভাবে পড়ুন।” বই (W0) সবার জন্য এক; প্রতিটি কোচিং ব্যাচের নিজের নোট-খাতা (BA). খাতা পাতলা, কারণ পরিবর্তনগুলো কয়েকটি মূল দিকেই।
৩ · Parameter আর memory হাতে গোনা
Interview-এ সবচেয়ে বেশি আসে এই হিসাব। নিয়মটি একটিই: প্রতিটি adapt করা Linear layer (in = k, out = d)-এ r(k + d)টি নতুন parameter. তারপর layer-এর সংখ্যা দিয়ে গুণ, আর যেগুলো পুরো শেখানো হয় (যেমন classifier head) সেগুলো যোগ।
| ধাপ | |
| হিসাব | |
| ফল |
- একটি Wq. BERT-base-এ hidden আকার ৭৬৮, তাই query projection ৭৬৮ × ৭৬৮ = ৫,৮৯,৮২৪টি ওজন। (Bias আলাদা, LoRA তা ছোঁয় না।)
- A আর B. A: ৮ × ৭৬৮ = ৬,১৪৪; B: ৭৬৮ × ৮ = ৬,১৪৪; মোট ১২,২৮৮, মূলের ২.০৮%.
- এক layer. LoRA প্রবন্ধের মতো query আর value দুটিতে: ২৪,৫৭৬।
- ১২টি layer. ১২ × ২৪,৫৭৬ = ২,৯৪,৯১২।
- Head. নতুন classifier (৭৬৮ → ২) পুরোটাই শিখতে হয়, কারণ সেটি শুরুতে এলোমেলো: ১,৫৩৮। মোট ২,৯৬,৪৫০।
- অনুপাত. ১৬.৪ কোটির মধ্যে মাত্র ০.১৮%. Embedding-এর ৭.৮ কোটি parameter (১,০২,০২৫ শব্দের vocabulary × ৭৬৮) একটিও শিখছে না।
peft library-ও ঠিক এই সংখ্যা দেয়। আর decoder model-এ? Qwen2.5-0.5B-এর (Apache 2.0) শুধু config নামিয়ে, ওজন না নামিয়ে, meta device-এ model বানিয়ে গোনা যায়। সত্যিকারের run, CPU-তে:
~/bazarghar-dl$ uv run python count_qwen.py
hidden 896 | layers 24 | ffn 4864 | kv heads 2 | vocab 151936 | tied True
['q_proj', 'v_proj'] r= 4 trainable 270,336 of 494,032,768 (0.055%)
['q_proj', 'v_proj'] r= 8 trainable 540,672 of 494,032,768 (0.109%)
['q_proj', 'v_proj'] r=32 trainable 2,162,688 of 494,032,768 (0.438%)
all-linear r= 8 trainable 4,399,104 of 494,032,768 (0.890%)
হাতে মিলিয়ে নিন। Qwen-এ grouped-query attention: ১৪টি query head কিন্তু মাত্র ২টি key/value head, প্রতিটি ৬৪ মাত্রার, তাই v_proj ৮৯৬ → ১২৮। r = ৮-এ q: ৮(৮৯৬ + ৮৯৬) = ১৪,৩৩৬; v: ৮(৮৯৬ + ১২৮) = ৮,১৯২; যোগ ২২,৫২৮; ২৪টি layer-এ ৫,৪০,৬৭২। হুবহু মিলল। “all-linear” সাতটি projection-এই (q, k, v, o, gate, up, down) LoRA বসায়, তাই আট গুণের বেশি।
এবার memory-র হিসাবটি নিজে চালান। একই Qwen-এর আকার থেকে full fine-tuning আর বিভিন্ন rank-এর LoRA:
# Qwen2.5-0.5B-এর config থেকে আকার (hidden 896, 24 layer, ffn 4864, kv 128)
h, layers, ffn, kv, vocab = 896, 24, 4864, 128, 151936
mods = {"q": (h, h), "k": (h, kv), "v": (h, kv), "o": (h, h),
"gate": (h, ffn), "up": (h, ffn), "down": (ffn, h)}
per_layer = sum(i * o for i, o in mods.values()) + h + 2 * kv + 2 * h # bias + norm
total = layers * per_layer + vocab * h + h # embedding (tied) + শেষ norm
GB = 1e9
def lora_params(r, names):
return layers * sum(r * (mods[m][0] + mods[m][1]) for m in names)
print(f"মোট parameter ≈ {total / 1e6:.1f} M")
full = total * 16 # fp32 w + grad + Adam m + v
print(f"full fine-tuning (fp32 + AdamW): {full / GB:5.2f} GB")
print(f" ওজন {total*4/GB:.2f} │ grad {total*4/GB:.2f} │ m+v {total*8/GB:.2f}")
print("target r │ trainable │ % │ bf16 base + adapter (GB)")
for names, label in [(("q", "v"), "q,v"), (tuple(mods), "all")]:
for r in (4, 8, 32):
p = lora_params(r, names)
mem = total * 2 + p * 16 # frozen bf16, adapter fp32 + grad + m + v
print(f"{label:5s} {r:4d} │ {p:10,d} │ {100 * p / total:5.2f} │ {mem / GB:.3f}")
lin = layers * sum(i * o for i, o in mods.values()) # শুধু Linear ওজন 4-bit হয়
bits = 4 + 8 / 64 + 32 / (64 * 256) # NF4 + double-quant ধ্রুবক
q = lin * bits / 8 + (total - lin) * 2 + lora_params(8, ("q", "v")) * 16
print(f"QLoRA q,v r=8: Linear {bits:.3f} bit/param, বাকি bf16 → {q / GB:.3f} GB")
print("activation বাদ; batch আর sequence দৈর্ঘ্যের ওপর নির্ভর করে")আউটপুট (পাতার Python-এ চালানো):
মোট parameter ≈ 494.0 M
full fine-tuning (fp32 + AdamW): 7.90 GB
ওজন 1.98 │ grad 1.98 │ m+v 3.95
target r │ trainable │ % │ bf16 base + adapter (GB)
q,v 4 │ 270,336 │ 0.05 │ 0.992
q,v 8 │ 540,672 │ 0.11 │ 0.997
q,v 32 │ 2,162,688 │ 0.44 │ 1.023
all 4 │ 2,199,552 │ 0.45 │ 1.023
all 8 │ 4,399,104 │ 0.89 │ 1.058
all 32 │ 17,596,416 │ 3.56 │ 1.270
QLoRA q,v r=8: Linear 4.127 bit/param, বাকি bf16 → 0.466 GB
activation বাদ; batch আর sequence দৈর্ঘ্যের ওপর নির্ভর করে
হিসাবের মোট ৪৯,৪০,৩২,৭৬৮, ওপরের meta-run-এর সঙ্গে হুবহু। Full-এ ৭.৯০ GB-এর অর্ধেক (৩.৯৫) শুধু Adam-এর m আর v. LoRA-তে memory-র প্রায় সবটাই frozen base (bf16-এ ০.৯৯ GB); r বাড়ালেও adapter-এর অংশ ছোট থাকে, all-linear r = ৩২-এও ০.২৮ GB. তাই LoRA-র পর বড় সাশ্রয়ের পরের ধাপ base-কেই ছোট করা: সেটি QLoRA (অংশ ৯)। মনে রাখবেন, activation এই হিসাবে নেই; অংশ ৭-এর মাপা peak-এ সেটি দেখা যাবে।
৪ · B = 0 দিয়ে শুরু, আর α/r scaling
LoRA প্রবন্ধে A শুরু হয় এলোমেলো মানে, আর B শূন্যে। peft-এর ডিফল্টও তাই: A-তে Kaiming-uniform, B-তে শূন্য (লেখার সময়ে peft-এর নথি অনুযায়ী)। ফল: শুরুতে BA = ০, তাই model হুবহু pretrained model. Training শুরু হয় একটি জানা, ভালো জায়গা থেকে, কোনো এলোমেলো ধাক্কা ছাড়া।
কিন্তু দুটিই শূন্য হলে? তখন A-র gradient-এ B থাকে আর B-র gradient-এ Ax; দুটিই শূন্য, কেউ কখনো নড়ত না। একটিকে এলোমেলো রাখলেই চলে। নিচের কোড NumPy-তে একটি LoRA layer: ধাপ ০-এ output মেলে কি না, কোন matrix gradient পায়, আর এক ধাপ পর কী হয়।
import numpy as np
rng = np.random.default_rng(42)
d_in, d_out, r, alpha, n = 32, 24, 2, 4, 5
W = rng.normal(0, 0.3, (d_out, d_in)) # frozen pretrained weight
X = rng.normal(0, 1, (n, d_in)) # 5টি input সারি
bound = np.sqrt(6 / d_in) # Kaiming-uniform-এর মতো সীমা
A = rng.uniform(-bound, bound, (r, d_in)) # A: এলোমেলো
B = np.zeros((d_out, r)) # B: শূন্য
s = alpha / r
def lora(X, A, B):
return X @ W.T + s * (X @ A.T) @ B.T # W x + (alpha/r) B A x
Y0 = lora(X, A, B)
print("step 0: max|lora - frozen| =", np.abs(Y0 - X @ W.T).max())
T = X @ W.T + rng.normal(0, 0.5, (n, d_out)) # নতুন কাজের লক্ষ্য
dY = (Y0 - T) / n # MSE loss-এর gradient (1/2 ধরে)
H = X @ A.T # (n, r): A-র পরে
gB = s * dY.T @ H # dL/dB
gA = s * (dY @ B).T @ X # dL/dA
print("|grad B| =", round(float(np.linalg.norm(gB)), 4),
"| |grad A| =", float(np.linalg.norm(gA)))
lr = 0.02
B1 = B - lr * gB # প্রথম ধাপে শুধু B নড়ে
loss = lambda Y: 0.5 * ((Y - T) ** 2).sum() / n
print("loss:", round(loss(Y0), 4), "->", round(loss(lora(X, A, B1)), 4))
print("rank(B1 @ A) =", np.linalg.matrix_rank(B1 @ A),
"| trainable:", A.size + B.size, "vs full:", W.size)আউটপুট (পাতার Python-এ চালানো):
step 0: max|lora - frozen| = 0.0
|grad B| = 3.595 | |grad A| = 0.0
loss: 2.5246 -> 2.283
rank(B1 @ A) = 2 | trainable: 112 vs full: 768
ধাপ ০-এ পার্থক্য ঠিক 0.0, আসন্ন মান নয়: শূন্য দিয়ে গুণ করলে শূন্যই। B-র gradient শূন্য নয়, কারণ তাতে Ax আছে; A-র gradient ঠিক শূন্য, কারণ তাতে B আছে। তাই প্রথম ধাপে শুধু B নড়ে, loss কমে, আর পরের ধাপ থেকে A-ও gradient পায়। শেখা পরিবর্তন B1A-র rank ২, অর্থাৎ r-এর বেশি নয়।
α/r কেন? r বদলালে BAx-এর মাপ বদলায়। লেখকেরা বলেছেন, α একবার ঠিক করে α/r দিয়ে গুণ করলে r বদলানোর পর learning rate নতুন করে খুঁজতে কম হয়। α = r হলে গুণক ১; আমাদের run-এ α = ২r, গুণক ২। এটি একটি সুবিধার নিয়ম, মৌলিক সূত্র নয়: Kalajdzievski (২০২৩) বড় r-এ α/√r (rsLoRA) প্রস্তাব করেন, peft-এ use_rslora=True. লেখার সময়ে peft-এর নথিতে ডিফল্ট r=8, lora_alpha=8; ডিফল্ট version-ভেদে বদলাতে পারে, তাই নিজে লিখে দিন।
| কী | |
| হিসাব | |
| h |
- মূল পথ. x = [১, ২, −১]. প্রথম সারি: ১ + ০ − ২ = −১; দ্বিতীয়: ০ + ২ + ১ = ৩। W0x = [−১, ৩]।
- সরু পথে চাপা. A একটি সারি (r = ১): ০.৫ − ২ − ১ = −২.৫। তিনটি সংখ্যা একটিতে নামল।
- শুরুতে B = 0. পাশের পথ শূন্য, h = [−১, ৩], হুবহু pretrained model.
- Training-এর পর. ধরুন B শিখে হলো [০.২, −০.৪]। পাশের পথ: [০.২ × −২.৫, −০.৪ × −২.৫] = [−০.৫, ১.০]।
- Scaling. α/r = ২/১ = ২: পাশের পথ [−১, ২]। যোগ করে h = [−২, ৫]।
- Merge. ২·BA = [[০.২, −০.৪, ০.৪], [−০.৪, ০.৮, −০.৮]]; W′ = [[১.২, −০.৪, ২.৪], [−০.৪, ১.৮, −১.৮]]; W′x = [১.২ − ০.৮ − ২.৪, −০.৪ + ৩.৬ + ১.৮] = [−২, ৫]। একই উত্তর, একটি গুণে।
৫ · Rank কত লাগে — SVD দিয়ে দেখা
কোনো ΔW-কে rank r-এর matrix দিয়ে সবচেয়ে ভালো কীভাবে কাছাকাছি আনা যায়? উত্তর truncated SVD: সবচেয়ে বড় r-টি singular value আর তাদের দিক রাখা (Eckart–Young উপপাদ্য; মেশিন লার্নিং কোর্সের পর্ব ৯)। এটি LoRA-র জন্য একটি “সেরা সম্ভাব্য” মাপকাঠি: rank r-এর কোনো BA এর চেয়ে কাছে যেতে পারে না। নিচে একটি জানা ΔW: আসলে rank ৪, সঙ্গে সামান্য গোলমাল।
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
d = k = 64
U0 = rng.normal(size=(d, 4)); V0 = rng.normal(size=(4, k))
dW = U0 @ np.diag([8, 5, 3, 2]) @ V0 / 8 # আসল পরিবর্তন: rank 4
dW_noisy = dW + rng.normal(0, 0.05, (d, k)) # সঙ্গে ছোট গোলমাল
U, S, Vt = np.linalg.svd(dW_noisy)
print("প্রথম ৬টি singular value:", np.round(S[:6], 2))
full = np.linalg.norm(dW_noisy)
print(" r │ params │ rel. error")
for r in [1, 2, 4, 8, 16, 64]:
approx = U[:, :r] * S[:r] @ Vt[:r] # B = U_r S_r, A = V_r^T
err = np.linalg.norm(dW_noisy - approx) / full
print(f"{r:2d} │ {r * (d + k):6d} │ {err:.4f}")
print("full ΔW params:", d * k)
fig, ax = plt.subplots(figsize=(5, 3))
ax.semilogy(np.arange(1, 65), S, "o-", ms=3)
ax.axvline(4.5, ls="--", c="gray")
ax.set_xlabel("index i"); ax.set_ylabel("singular value (log)")
ax.set_title("Spectrum of a rank-4 update plus noise")
plt.show()আউটপুট (পাতার Python-এ চালানো):
প্রথম ৬টি singular value: [57.92 34.13 22.95 14.83 0.76 0.72]
r │ params │ rel. error
1 │ 128 │ 0.6034
2 │ 256 │ 0.3784
4 │ 512 │ 0.0412
8 │ 1024 │ 0.0363
16 │ 2048 │ 0.0282
64 │ 8192 │ 0.0000
full ΔW params: 4096
প্রথম চারটি singular value বড়, পঞ্চমটি থেকে হঠাৎ পতন (৫৭.৯২, ৩৪.১৩, ২২.৯৫, ১৪.৮৩, তারপর ০.৭৬)। Graph-এ এই খাদটিই চোখে পড়ে। r = ৪-এ ত্রুটি ৪.১%, আর r বাড়িয়ে ১৬ করলেও মাত্র ২.৮%: বাড়তি rank শুধু গোলমাল ধরছে। ৫১২টি সংখ্যায় (৪,০৯৬-এর ১২.৫%) প্রায় পুরো তথ্য। r = ৬৪-এ ত্রুটি শূন্য, কিন্তু তখন r(d + k) = ৮,১৯২, পুরো matrix-এর দ্বিগুণ: পূর্ণ rank-এ LoRA লাভজনক নয়।
সাবধান: LoRA training-এ SVD চলে না; B আর A gradient দিয়ে সরাসরি শেখে। আর বাস্তব কাজের ΔW আগে থেকে জানা থাকে না, তাই এখানের মতো পরিষ্কার খাদ নাও থাকতে পারে। LoRA প্রবন্ধ GPT-3-এ খুব ছোট r (১ বা ২)-কেও কয়েকটি কাজে যথেষ্ট পেয়েছিল; সেটি সেই model আর কাজের ফল। নিজের কাজে r বাছাই মেপে করুন, যেমন অংশ ৭-এ।
৬ · Target modules আর peft-এ LoraConfig
কোন layer-এ LoRA বসবে, সেটি target_modules. নামগুলো architecture-এর নিজের: BERT-এ query, key, value, dense; Qwen বা Llama-ধাঁচের model-এ q_proj, v_proj, gate_proj ইত্যাদি। নাম ভুল দিলে peft error দেয়; তাই আগে model.named_modules() print করে দেখুন। আমাদের run-এর মূল অংশ (পুরো script থেকে ছোট করা):
from peft import LoraConfig, get_peft_model
cfg = LoraConfig(task_type="SEQ_CLS", r=8, lora_alpha=16,
lora_dropout=0.1, target_modules=["query", "value"])
model = get_peft_model(model, cfg) # base-এর সব ওজন frozen
model.print_trainable_parameters()
opt = torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr=5e-4)
trainable params: 296,450 || all params: 164,694,532 || trainable%: 0.1800
সংখ্যাটি অংশ ৩-এর হাতের হিসাবের সঙ্গে হুবহু: ২,৯৬,৪৫০। task_type="SEQ_CLS" দিলে peft নিজে classifier head-কে modules_to_save-এ রাখে, মানে head পুরোটা শেখে আর adapter ফাইলে সংরক্ষিত হয়। এটি ভুলে গেলে head এলোমেলো থেকে যায় আর ফল প্রায় আন্দাজের সমান হয়, AI-এর লেখা কোডে এই ভুল প্রায়ই দেখা যায়। LoRA-তে learning rate সাধারণত full-এর চেয়ে বড় (আমাদের ৫ × ১০−৪ বনাম ২ × ১০−৫), কারণ অল্প কয়েকটি parameter অনেক দূর যেতে হয়।
কোন module বাছবেন, তার সার্বজনীন উত্তর নেই। LoRA প্রবন্ধ GPT-3-এ সীমিত parameter-এর বাজেটে Wq আর Wv দুটিতে ছোট r-কে একটিতে বড় r-এর চেয়ে ভালো পেয়েছিল। QLoRA প্রবন্ধ বলে, তাদের পরীক্ষায় সব Linear layer-এ LoRA বসানো full fine-tuning-এর মানে পৌঁছাতে জরুরি ছিল। দুটিই নিজের পরীক্ষার ফল; বাজেট আর কাজ বুঝে দুটিই চেষ্টা করে মাপুন।
৭ · সত্যিকারের run — full বনাম LoRA r = ৪, ৮, ৩২
একই কাজ, একই ডেটা, একই script, শুধু শেখার পদ্ধতি আলাদা। ডেটা পর্ব ৪২ · Text classification-এর Sazzed-এর বাংলা YouTube মন্তব্য (Mendeley Data, CC BY 4.0), পরিষ্কার করার পর ১০,৭০৯টি, positive/negative. শুধু প্রথম split (repeat ০, fold ০): train ৭,৫১০, validation ১,০৫৬, test ২,১৪৩, একই commenter দুই ভাগে পড়ে না। Model sagorsarker/bangla-bert-base, do_lower_case=False (পর্ব ৪৮-এর ফাঁদ মনে রেখে), সর্বোচ্চ ১০০ token, batch ৩২, ৩ epoch, AdamW, warm-up-সহ রৈখিক schedule, bf16 autocast, class weight, seed ০। Full-এ lr ২ × ১০−৫, LoRA-তে ৫ × ১০−৪, α = ২r, dropout ০.১। সব run GPU-তে, প্রতিটি আলাদা process-এ। সত্যিকারের run-এর ফল, script-এর JSON লাইন থেকে সাজানো:
| পদ্ধতি | শেখার parameter | % | Test macro-F1 | Peak MiB | সময় (s) |
|---|---|---|---|---|---|
| Full fine-tuning | ১৬,৪৩,৯৮,০৮২ | ১০০ | ০.৯৪২৫ | ৩,৯৬৯ | ১০০.৫ |
| LoRA r = ৪ (q, v) | ১,৪৮,৯৯৪ | ০.০৯ | ০.৯২৯০ | ১,৭৬৪ | ৫৭.৪ |
| LoRA r = ৮ (q, v) | ২,৯৬,৪৫০ | ০.১৮ | ০.৯৩৩২ | ১,৭৬৭ | ৫৫.৭ |
| LoRA r = ৩২ (q, v) | ১১,৮১,১৮৬ | ০.৭২ | ০.৯৩৩৬ | ১,৭৮৬ | ৫৬.৮ |
| LoRA r = ৮ (all-linear) | ১৩,৪০,৯৩০ | ০.৮২ | ০.৯৩০৬ | ২,৪৫২ | ৮৬.৭ |
| QLoRA r = ৮ (q, v), NF4 | ২,৯৬,৪৫০ | ০.১৮ | ০.৯২৫০ | ১,৫৮০ | ৭৪.১ |
Epoch-ভিত্তিক validation (সত্যিকারের log; প্রতিটি run-এর তিনটি লাইন এক লাইনে সাজানো):
full epoch 1 val macro-F1 0.9354 | 2: 0.9443 | 3: 0.9498
lora r=8 qv epoch 1 val macro-F1 0.8935 | 2: 0.9301 | 3: 0.9348
মান: এই split-এ full fine-tuning এগিয়ে, test-এ প্রায় ০.০০৯ থেকে ০.০১৪, validation-এও। LoRA-র তিনটি rank নিজেদের মধ্যে খুব কাছে (০.৯২৯ থেকে ০.৯৩৪): r = ৪ থেকে ৩২-এ parameter আট গুণ, মান প্রায় এক। LoRA-র validation তৃতীয় epoch-এও বাড়ছিল, তাই হয়তো আরও epoch বা বড় lr-এ ফাঁক কমত; সেটি আমরা মাপিনি। একই split-এ TF-IDF + logistic regression পেয়েছিল ০.৯৪১২ (পর্ব ৪২)। এটি একটি split আর একটি seed: “LoRA full-এর চেয়ে ১ পয়েন্ট খারাপ” সাধারণ সত্য নয়, শুধু এই run-এর পর্যবেক্ষণ।
Memory: peak ৩,৯৬৯ থেকে ১,৭৬৭ MiB, অর্থাৎ ৫৫% কম; parameter ৫৫০ গুণ কমলেও memory আড়াই গুণও কমেনি। কারণ অংশ ১-এর stepper-এ: frozen ওজন (৬২৭ MiB) আর activation থেকেই যায়। All-linear-এ LoRA layer বেশি, তাই জমা রাখা মাঝের ফলও বেশি: ২,৪৫২ MiB আর ৮৭ সেকেন্ড। সময়: LoRA প্রায় অর্ধেক সময়ে, কারণ frozen ওজনের gradient (dL/dW) হিসাব হয় না, যদিও input-এর gradient পেছনে পাঠাতে হয়।
| প্রশ্ন | |
| সংখ্যা | |
| শিক্ষা |
- Rank. r = ৪ থেকে ৩২-এ শেখার parameter ১,৪৮,৯৯৪ থেকে ১১,৮১,১৮৬, কিন্তু test macro-F1 ০.৯২৯০ থেকে ০.৯৩৩৬। অংশ ৫-এর SVD-র মতো: পরিবর্তনের মূল দিক অল্প হলে বাড়তি rank প্রায় কিছু যোগ করে না।
- Full বনাম LoRA. Full ০.৯৪২৫, LoRA r = ৮ ০.৯৩৩২। এই ছোট model-এ full-এর পুরো memory আমাদের সাধ্যের মধ্যেই, তাই এখানে full-ই যুক্তিসঙ্গত। পার্থক্যটি একটি run-এর; সিদ্ধান্তের আগে কয়েকটি seed আর split লাগবে।
- Memory. ২,২০২ MiB কমেছে; হাতের হিসাবে gradient আর Adam-এর state-এ ১,৮৭৭ MiB কমার কথা। বাকি দুটি run-এই থাকে: frozen ওজন আর ১,১০০ MiB-এর মতো activation ও কাজের জায়গা।
- Target. সব Linear-এ বসালে parameter সাড়ে চার গুণ, memory আর সময় বাড়ল, মান বাড়েনি। QLoRA প্রবন্ধ সব Linear-কে জরুরি বলেছিল তাদের বড় model আর কাজে; এখানে তা দেখা যায়নি।
- QLoRA. Load-এর পর memory ৬২৯ থেকে ৩৪৪ MiB, peak ১,৫৮০ MiB; কিন্তু ৩৩% ধীর (dequantize-এর খরচ) আর test ০.৯২৫০। ছোট model-এ শুধু activation-ই peak-এর বড় অংশ, তাই QLoRA-র আসল লাভ বড় model-এ।
৮ · Merge — adapter ওজনে মেশানো, আর কখন মেশাবেন না
Training-এর সময় দুটি পথ: W0x আর (α/r)BAx. Inference-এ এটি একটু বাড়তি হিসাব। কিন্তু গুণ বণ্টনযোগ্য: W0x + (α/r)BAx = (W0 + (α/r)BA)x. তাই একবার W′ = W0 + (α/r)BA বানিয়ে নিলে model আবার একটি সাধারণ model, বাড়তি latency শূন্য। LoRA প্রবন্ধের “no additional inference latency” দাবি এখান থেকেই। চালিয়ে দেখুন, আর precision কমালে কী হয় তাও:
import numpy as np
rng = np.random.default_rng(7)
d, r, alpha = 32, 8, 16
s = alpha / r
W = rng.normal(0, 0.2, (d, d)) # frozen ওজন
A = rng.normal(0, 0.1, (r, d)) # "শেখা" adapter (ধরে নেওয়া)
B = rng.normal(0, 0.1, (d, r))
X = rng.normal(0, 1, (100, d))
two_path = X @ W.T + s * (X @ A.T) @ B.T # training-এর সময়ের দুই পথ
W_merged = W + s * B @ A # merge: একটি matrix
one_path = X @ W_merged.T
print("float64 merge: max diff =", f"{np.abs(two_path - one_path).max():.1e}")
for dt in (np.float32, np.float16): # কম precision-এ merge করলে?
Wm = (W.astype(dt) + (s * B @ A).astype(dt)).astype(dt)
y = X.astype(dt) @ Wm.T
print(f"{np.dtype(dt).name:8s} merge: max diff =",
f"{np.abs(two_path - y.astype(np.float64)).max():.1e}")
W_back = W_merged - s * B @ A # unmerge: আবার আলাদা করা
print("unmerge ফিরে পেল W? ", np.allclose(W_back, W, atol=1e-12))
B2 = rng.normal(0, 0.1, (d, r)); A2 = rng.normal(0, 0.1, (r, d))
W_task2 = W + s * B2 @ A2 # একই W, দ্বিতীয় কাজের adapter
print("adapter-এর আকার:", A.size + B.size, "সংখ্যা; W:", W.size)
print("দুই কাজের output আলাদা:", not np.allclose(X @ W_task2.T, one_path))আউটপুট (পাতার Python-এ চালানো):
float64 merge: max diff = 2.7e-15
float32 merge: max diff = 9.0e-07
float16 merge: max diff = 2.0e-03
unmerge ফিরে পেল W? True
adapter-এর আকার: 512 সংখ্যা; W: 1024
দুই কাজের output আলাদা: True
float64-এ পার্থক্য প্রায় ৩ × ১০−১৫: শুধু যোগের ক্রম বদলের rounding, গণিতে দুটি এক। float32-এ ~১০−৬, float16-এ ২ × ১০−৩: কম bit-এ merge করলে ছোট ΔW-এর কিছু অংশ rounding-এ হারায়। 4-bit base-এর ওপর শেখা QLoRA adapter merge করতে সাধারণত base-কে আগে bf16-এ ফেরানো হয়, আর তখন output হুবহু এক থাকে না; merge-এর পর আবার মাপুন। Unmerge, অর্থাৎ বিয়োগ করে W ফেরত পাওয়া, float64-এ কাজ করে।
এবার সত্যিকারের model-এ। অংশ ৭-এর r = ৮ adapter (query, value) লোড করে CPU-তে fp32-এ ৫১২টি test মন্তব্যের logits নিলাম, তারপর peft-এর merge_and_unload() দিয়ে মিশিয়ে আবার। সত্যিকারের run:
~/bazarghar-dl$ uv run python merge_check.py
lora_A modules before merge: 24 | lora modules after: 0
type after merge: BertForSequenceClassification
max |logit diff| = 1.19e-06
same predicted label: 512 / 512
CPU time 512 texts: adapter 26.5 s | merged 27.2 s
adapter_model.safetensors: 1.14 MiB
merged model.safetensors: 627.2 MiB
১২টি layer × ২ = ২৪টি LoRA module merge-এর পর শূন্য, আর model আবার সাধারণ BertForSequenceClassification. Logits-এর সর্বোচ্চ পার্থক্য ১.২ × ১০−৬, float32-এর rounding-এর মাপ, আর ৫১২টির ৫১২টিতেই একই label. সময়ের পার্থক্য এখানে চোখে পড়েনি: r = ৮-এর পাশের পথ ৭৬৮ মাপের layer-এর তুলনায় খুব সস্তা, আর একবারের CPU মাপে এক সেকেন্ডের ওঠানামা স্বাভাবিক। আকারের পার্থক্য বড়: adapter ১.১৪ MiB (head-সহ), merge করা পুরো model ৬২৭.২ MiB, প্রায় ৫৫০ গুণ।
কখন merge করবেন না? যখন একটি base-এর ওপর অনেক কাজ। বাজারঘরের একটি server-এ একটি Bangla BERT, সঙ্গে রিভিউয়ের মনোভাব, অভিযোগের ধরন আর ডেলিভারির প্রশ্নের তিনটি ছোট adapter: অনুরোধ অনুযায়ী adapter বদলানো যায় (peft-এ load_adapter আর set_adapter). Merge করলে প্রতিটি কাজের জন্য আবার পুরো কপি। আর একটি জিনিস: adapter কাজ করে শুধু সেই base-এর সঙ্গে, যার ওপর শেখানো হয়েছে। Base-এর আরেকটি revision বা আরেকটি model-এ লাগালে নীরবে ভুল ফল আসতে পারে; তাই adapter-এর সঙ্গে base-এর নাম আর commit hash লিখে রাখুন (পর্ব ৫৩)।
৯ · QLoRA, আর adapter, prefix, prompt tuning ও IA3
অংশ ৩-এর হিসাবে LoRA-র পর memory-র বড় অংশ frozen base. QLoRA (Dettmers, Pagnoni, Holtzman ও Zettlemoyer, ২০২৩) সেই base-কেই 4-bit-এ রাখে আর তার ওপর LoRA শেখায়। প্রবন্ধের দাবি, এতে ৬৫০০ কোটি parameter-এর একটি model একটি ৪৮ GB GPU-তে fine-tune করা গেছে, তাঁদের পরীক্ষায় 16-bit fine-tuning-এর মান ধরে রেখে। তিনটি কৌশল:
NF4
4-bit NormalFloat: ১৬টি মান normal বণ্টনের quantile থেকে, কারণ pretrained ওজন মোটামুটি normal-ধাঁচের। ৬৪টি ওজনের প্রতিটি block-এর নিজের scale.
Double quantization
প্রতি block-এর fp32 scale মানে প্রতি parameter-এ ৩২/৬৪ = ০.৫ bit. Scale-গুলোকেও 8-bit-এ (২৫৬-এর block-এ) রাখলে ৮/৬৪ + ৩২/(৬৪·২৫৬) ≈ ০.১২৭ bit.
Paged optimizer
হঠাৎ memory বেড়ে গেলে optimizer state সাময়িকভাবে CPU RAM-এ সরে যায় (NVIDIA unified memory), out-of-memory-তে run ভাঙে না।
Forward-এর সময় প্রতিটি 4-bit block bf16-এ ফিরিয়ে (dequantize) গুণ হয়; gradient frozen 4-bit ওজনের ভেতর দিয়ে LoRA-তে পৌঁছায়, কিন্তু 4-bit ওজন নিজে কখনো বদলায় না। Quantization-এর scale আর zero point হাতে হিসাব পর্ব ৫৯ · ছোট আর দ্রুত model — quantization, distillation আর pruning-এ।
Windows-এ bitsandbytes একসময় আনুষ্ঠানিকভাবে সমর্থিত ছিল না; আমাদের lab-এ লেখার সময়ের version 0.50.2 install হয়েছে আর GPU-তে 4-bit চলেছে। একই কাজ, একই split, query আর value-তে r = ৮, base NF4 আর double quantization-এ, PagedAdamW দিয়ে। সত্যিকারের run, GPU-তে:
bnb_cfg = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=torch.bfloat16,
llm_int8_skip_modules=["classifier"])
model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=False)
opt = bnb.optim.PagedAdamW(params, lr=5e-4, weight_decay=0.01) # (script থেকে ছোট করা)
trainable params: 296,450 || all params: 164,694,532 || trainable%: 0.1800
epoch 1 val macro-F1 0.9066
epoch 2 val macro-F1 0.9358
epoch 3 val macro-F1 0.9327
{"tag": "qlora r=8 qv", "total": 121635842, "trainable": 296450, "test_macro": 0.9250449450516139, "secs": 74.1, "peak_mib": 1580, "mem_after_load_mib": 344}
শেষ লাইনটি script-এর JSON থেকে কয়েকটি চাবি বাদ দিয়ে ছোট করা। total ১২,১৬,৩৫,৮৪২ দেখাচ্ছে, ১৬.৪ কোটি নয়: bitsandbytes দুটি 4-bit মান এক byte-এ রাখে, তাই quantize করা Linear ওজনের গণনা অর্ধেক দেখায়। Encoder আর pooler-এর Linear ওজন ৮,৫৫,২৪,৪৮০টি; অর্ধেক বাদ দিলে ঠিক এই সংখ্যা। Embedding-এর ৭.৮ কোটি parameter Linear নয়, তাই 4-bit হয়নি; এই model-এর প্রায় অর্ধেক ওজন embedding-এ, ফলে load-এর পরের memory ৬২৯ থেকে মাত্র ৩৪৪ MiB-এ নেমেছে, চার ভাগের এক ভাগে নয়। Validation দ্বিতীয় epoch-এ সর্বোচ্চ, তারপর সামান্য কমেছে; test ০.৯২৫০, সাধারণ LoRA-র ০.৯৩৩২-এর চেয়ে একটু কম, একটি seed-এ। Decoder LLM-এ, যেখানে Linear ওজনই বেশির ভাগ, 4-bit-এর লাভ অনেক বড় হয়: অংশ ৩-এর হিসাবে Qwen2.5-0.5B-এর base ০.৯৯ থেকে প্রায় ০.৪৭ GB.
LoRA-র আত্মীয়রা (সংক্ষেপে). সব PEFT পদ্ধতির লক্ষ্য এক: base স্থির, শেখে অল্প কিছু। পার্থক্য কোথায় শেখে তাতে।
| পদ্ধতি | কী শেখে | Merge করা যায়? |
|---|---|---|
| Adapter (Houlsby ও সহলেখক, ২০১৯) | প্রতি layer-এ ছোট bottleneck MLP (নিচে নামানো, nonlinearity, ওপরে তোলা) | না, nonlinearity আছে; inference-এ বাড়তি layer |
| Prefix tuning (Li ও Liang, ২০২১) | প্রতি layer-এর key আর value-র আগে কয়েকটি শেখা vector | না; context-এর কিছু জায়গা নেয় |
| Prompt tuning (Lester ও সহলেখক, ২০২১) | শুধু input-এ কয়েকটি শেখা embedding | না; সবচেয়ে কম parameter |
| IA3 (Liu ও সহলেখক, ২০২২) | Key, value আর FFN activation-এর গুণক vector | হ্যাঁ, গুণক ওজনে মেশানো যায় |
| LoRA (Hu ও সহলেখক, ২০২১) | নির্বাচিত Linear-এ ΔW = BA | হ্যাঁ |
Lester ও সহলেখকদের পরীক্ষায় T5-এ prompt tuning model যত বড়, full fine-tuning-এর তত কাছে গেছে, ছোট model-এ পিছিয়ে ছিল। কোনটি “সেরা”, তার কোনো সার্বজনীন উত্তর নেই; LoRA এখন বেশি প্রচলিত, মূলত merge আর সহজ tooling-এর জন্য। Instruction ডেটায় LoRA দিয়ে SFT পর্ব ৫৬ · Instruction tuning — SFT ডেটা, chat template আর ফরম্যাট-এ, আর preference-এ পর্ব ৫৭ · Preference alignment — RLHF, reward model আর DPO-তে।
আরও গভীরে: LoRA প্রবন্ধ (Hu ও সহলেখক, arXiv ২১০৬.০৯৬৮৫) GPT-3 ১৭৫B-তে Adam-এর full fine-tuning-এর তুলনায় শেখার parameter প্রায় ১০,০০০ গুণ আর GPU memory প্রায় ৩ গুণ কমানোর কথা জানায়, আর শেখা ΔW-এর দিক W-এর কোন দিকগুলোকে বাড়ায় তা বিশ্লেষণ করে। QLoRA প্রবন্ধ (arXiv ২৩০৫.১৪৩১৪) NF4-কে normal বণ্টনের ওজনের জন্য তথ্যতত্ত্বের দিক থেকে সর্বোত্তম বলে দাবি করে। পরের কাজগুলো (যেমন DoRA, PiSSA-র মতো আরম্ভের কৌশল) LoRA-কে মাপ আর দিকে ভাগ করা বা SVD দিয়ে শুরু করার প্রস্তাব দেয়; peft-এ এগুলোর কিছু আছে, তবে প্রতিটির দাবি নিজের কাজে যাচাই সাপেক্ষ।
পরীক্ষা ও ইন্টারভিউতে:
- ৪০৯৬ × ৪০৯৬ Linear-এ r = ১৬ LoRA-র parameter? ১৬ × (৪০৯৬ + ৪০৯৬) = ১,৩১,০৭২, মূলের ০.৭৮%.
- fp32 আর AdamW-তে ১০০ কোটি parameter-এর full fine-tuning-এ activation বাদে memory? ১৬ byte × ১০৯ = ১৬ GB.
- B কেন শূন্যে শুরু? যাতে শুরুতে ΔW = ০, model হুবহু pretrained; A এলোমেলো, তাই B gradient পায়।
- Merge-এর পর latency? বাড়তি শূন্য: W′ = W + (α/r)BA একটিই matrix.
- QLoRA-র তিনটি কৌশল? NF4, double quantization, paged optimizer; base 4-bit-এ স্থির, LoRA bf16-এ শেখে।
১০ · AI-এর যুগে — LoRA কোড যাচাই
LoRA-র কোড AI assistant খুব দ্রুত লেখে, কিন্তু peft, transformers আর bitsandbytes তিনটিই দ্রুত বদলায়, আর অনলাইনের উদাহরণ নানা version-এর।
| AI প্রায়ই যা করে | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
অন্য architecture-এর target_modules (q_proj BERT-এ) | নাম মেলে না, error বা ভুল layer | named_modules() print করা |
| Classifier head-এর কথা ভুলে যাওয়া | এলোমেলো head, ফল আন্দাজের কাছে | print_trainable_parameters() হাতে গোনার সঙ্গে মেলানো |
পুরনো argument (load_in_4bit=True সরাসরি from_pretrained-এ) | নতুন version-এ BitsAndBytesConfig লাগে | নিজের version-এর নথি পড়া |
| Full-এর learning rate LoRA-তে | প্রায়ই খুব ধীরে শেখে | ছোট lr sweep, validation-এ মাপা |
merge_and_unload()-এর ফল assign না করা | মেশানো model ফেলে দেওয়া হয় | Merge-এর আগে-পরে logits তুলনা |
| “LoRA = full-এর সমান মান” বলা | কাজ আর rank-এর ওপর নির্ভর করে | একই split-এ full আর baseline-এর সঙ্গে, কয়েকটি seed-এ |
কোড চাওয়ার সময় যাচাইও চেয়ে নিন:
AI-এর লেখা LoRA কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১কোটি কোটি parameter-এর model-কে মাত্র ০.১% parameter শিখিয়ে নতুন কাজে মানানো যায় কেন? “Low rank” অনুমানের পেছনের প্রমাণ কতটা শক্ত?
গল্পটি শুরু হয় ২০১৮ সালে। Li ও সহলেখকেরা “intrinsic dimension” মাপার একটি পরীক্ষা প্রস্তাব করেন: সব parameter না শিখিয়ে, একটি এলোমেলো কম-মাত্রার উপস্থানে শেখান, আর দেখুন কত মাত্রায় ফল প্রায় পুরো training-এর কাছে পৌঁছায়। অনেক কাজে সংখ্যাটি parameter-এর মোট সংখ্যার তুলনায় খুব ছোট এল। ২০২০ সালে Aghajanyan ও সহলেখকেরা একই প্রশ্ন pretrained language model-এর fine-tuning-এ করেন, আর দেখান pretrained model যত বড় আর যত বেশি pretrain করা, fine-tuning-এর intrinsic dimension তত ছোট হওয়ার প্রবণতা। তাঁদের ব্যাখ্যা: pretraining এমন একটি প্রতিনিধিত্ব শেখায়, যেখান থেকে নতুন কাজে যেতে অল্প কয়েকটি দিকে সরলেই চলে।
LoRA প্রবন্ধ এই ধারণাকে ওজনের স্তরে আনে: শুধু parameter-এর উপস্থান নয়, প্রতিটি matrix-এর পরিবর্তন ΔW নিজেই কম rank-এর হতে পারে। লেখকেরা GPT-3-এ দেখান, r = ১ বা ২-এও কিছু কাজে ভালো ফল আসে, আর বিভিন্ন r-এ শেখা ΔW-এর মূল দিকগুলো অনেকটা মিলে যায়। আমাদের ছোট পরীক্ষায়ও r = ৪ আর r = ৩২-এর test macro-F1 কাছাকাছি (০.৯২৯ আর ০.৯৩৪, একটি seed).
তবে প্রমাণের সীমা আছে। এক, এগুলো পর্যবেক্ষণ, কোনো উপপাদ্য নয়। দুই, বেশির ভাগ পরীক্ষা ইংরেজি বা ভালোভাবে pretrain করা ক্ষেত্রে। কাজ যত দূরের (নতুন লিপি, কোড থেকে চিকিৎসা নথি, base model যে ভাষা কম দেখেছে), পরিবর্তন তত বড় আর জটিল হতে পারে। Biderman ও সহলেখকদের ২০২৪ সালের একটি গবেষণার শিরোনামই “LoRA Learns Less and Forgets Less”: তাঁদের কোড আর গণিতের দীর্ঘ training-এ LoRA full fine-tuning-এর চেয়ে কম শিখেছে, আবার আগের জ্ঞান কম ভুলেছে। তিন, rank কম মানে প্রকাশের ক্ষমতা কম; এটি একটি regularizer-এর মতোও কাজ করে, যা ছোট ডেটায় ভালো, বড় ডেটায় সীমা।
মূল উপলব্ধি: Low-rank অনুমান একটি ভালো সমর্থিত পর্যবেক্ষণ, প্রকৃতির নিয়ম নয়। কাজ base model-এর জ্ঞানের যত কাছে, ছোট r তত নিরাপদ; দূরের কাজে r আর target বাড়িয়ে, আর full-এর সঙ্গে তুলনা করে দেখুন।
প্র ০২বাজারঘর একটি বাংলা classifier বানাবে। Full fine-tuning না LoRA — কোন কোন বিষয় দেখে সিদ্ধান্ত নেবেন?
প্রথমে মান। আমাদের run-এ একই split-এ full fine-tuning ০.৯৪২৫ আর LoRA r = ৮ ০.৯৩৩২ পেয়েছে, একটি seed-এ; পার্থক্য ছোট, কিন্তু full-এর দিকে। আবার পর্ব ৪৮-এ পাঁচটি fold আর তিনটি repeat-এ দেখা গিয়েছিল, এই ডেটায় Bangla BERT আর TF-IDF-এর মধ্যে স্পষ্ট বিজয়ী নেই; তাই সবচেয়ে সস্তা পদ্ধতিটিও তালিকায় রাখুন।
দ্বিতীয়ত memory আর হার্ডওয়্যার। ১৬ কোটির model-এ full fine-tuning একটি সাধারণ GPU-তেই চলে, যেমন আমাদের run-এ চলেছে। কিন্তু ৭০০ কোটির একটি model-এ fp32 AdamW-এর হিসাবেই ১১২ GB, activation বাদে; সেখানে LoRA বা QLoRA প্রায় একমাত্র উপায়, যদি না অনেক GPU আর ZeRO-ধাঁচের ভাগাভাগি থাকে।
তৃতীয়ত কাজের সংখ্যা আর জমা রাখা। পাঁচটি কাজে পাঁচটি পুরো কপি বনাম একটি base আর পাঁচটি ছোট adapter. Adapter বদলে বদলে একই server-এ চালানো যায়, আর নতুন কাজ যোগ করতে base ছুঁতে হয় না। চতুর্থত ভুলে যাওয়া: full fine-tuning catastrophic forgetting-এর ঝুঁকি বেশি; LoRA-তে base অক্ষত থাকে, adapter খুলে ফেললেই আগের model.
পঞ্চমত সময় আর পরীক্ষার খরচ। LoRA-র hyperparameter বেশি: r, α, target, dropout, আর ভিন্ন learning rate. ছোট model-এ full fine-tuning-এর একটি ভালো জানা রেসিপি (পর্ব ৫৪) প্রায়ই কম ঝামেলার। শেষে লাইসেন্স: adapter base-এর ওপর নির্ভরশীল, তাই base-এর লাইসেন্সের শর্ত adapter বিতরণেও প্রযোজ্য হতে পারে।
মূল উপলব্ধি: ছোট model আর একটি কাজে full fine-tuning যুক্তিসঙ্গত; বড় model, অনেক কাজ বা সীমিত GPU-তে LoRA. যেটিই নিন, সরল baseline আর একাধিক seed-এর সঙ্গে মাপা ফল দেখে সিদ্ধান্ত নিন।
প্র ০৩QLoRA কীভাবে “একটি GPU-তে বড় model fine-tuning” সম্ভব করল? এর পেছনের ইতিহাস আর দাম কী?
QLoRA একদিনে আসেনি। Tim Dettmers ও সহলেখকেরা ২০২১ সালে 8-bit optimizer দেখান: Adam-এর m আর v-কে block-ভিত্তিক quantization দিয়ে 8-bit-এ রাখা, ফলে optimizer-এর memory প্রায় চার ভাগের এক ভাগ। ২০২২ সালে LLM.int8() প্রবন্ধে তাঁরা দেখান, বড় model-এ কিছু activation-এর মান খুব বড় (outlier), আর সেগুলো আলাদা করে 16-bit-এ রেখে বাকিটা 8-bit-এ গুণ করলে মান প্রায় অক্ষত থাকে। এই কাজগুলোর ফসল bitsandbytes library.
২০২৩ সালের মে মাসে QLoRA প্রবন্ধ তিনটি ধারণা জোড়ে। NF4: যেহেতু pretrained ওজন মোটামুটি শূন্যকে কেন্দ্র করে normal-ধাঁচের, ১৬টি মান সমান দূরত্বে না রেখে normal বণ্টনের quantile-এ রাখা। Double quantization: প্রতি ৬৪টি ওজনের block-এর scale-ও আবার quantize করা, প্রতি parameter-এ প্রায় ০.৩৭ bit সাশ্রয়, প্রবন্ধের হিসাবে ৬৫B model-এ প্রায় ৩ GB. Paged optimizer: হঠাৎ লম্বা sequence এলে optimizer state CPU-তে সরিয়ে out-of-memory ঠেকানো। লেখকেরা জানান, এভাবে ৬৫B model একটি ৪৮ GB GPU-তে fine-tune হয়েছে, আর তাঁদের মূল্যায়নে 16-bit-এর মান ধরে রেখেছে।
দাম কী? এক, গতি: প্রতিটি forward-এ ওজন dequantize করতে হয়; আমাদের ছোট run-এ সময় আর memory দুটিই মাপা আছে, আর ছোট model-এ লাভ সীমিত, কারণ embedding আর activation-এর অংশ বড়। দুই, merge: 4-bit base-এ শেখা adapter-কে merge করতে base-কে bf16-এ তুলতে হয়, তখন output সামান্য বদলায়। তিন, হার্ডওয়্যার আর platform: bitsandbytes মূলত NVIDIA GPU-কেন্দ্রিক ছিল, আর Windows সমর্থন পরে এসেছে; আমাদের lab-এ চলেছে, কিন্তু প্রতিটি মেশিনে যাচাই দরকার।
মূল উপলব্ধি: QLoRA memory-কে তিন জায়গায় চাপে: base-এর ওজন, quantization-এর ধ্রুবক আর optimizer-এর হঠাৎ চাহিদা। বিনিময়ে গতি আর merge-এর নির্ভুলতায় কিছু দাম দিতে হয়; মান যাচাই নিজের কাজে।
প্র ০৪একটি base model-এর ওপর শত শত adapter — production-এ এটি কীভাবে চালানো হয়, আর এর ঝুঁকি কী?
LoRA-র একটি বড় সুবিধা হলো adapter ছোট। আমাদের r = ৮ adapter (head-সহ) মাত্র কয়েক MB, যেখানে পুরো model শত শত MB. তাই একটি server-এ একটি base রেখে অনেক গ্রাহক বা অনেক কাজের adapter রাখা যায়। সরল পথ: অনুরোধ এলে peft-এ set_adapter দিয়ে adapter বদলানো। কিন্তু একই batch-এ আলাদা adapter-এর অনুরোধ এলে? তখন base-এর গুণ একসঙ্গে, আর প্রতিটি সারির LoRA অংশ আলাদা করে করতে হয়।
গবেষণায় এর জন্য বিশেষ ব্যবস্থা প্রস্তাবিত হয়েছে। Sheng ও সহলেখকদের S-LoRA (২০২৩) হাজারো adapter-কে CPU আর GPU memory-র মধ্যে আনা-নেওয়া করে আর আলাদা adapter-এর গুণ একসঙ্গে চালানোর kernel লেখে; Punica একই ধরনের ধারণা নিয়ে কাজ করে। কিছু inference server এখন একাধিক LoRA adapter একসঙ্গে চালানোর সুবিধা নথিতে দেয়; ঠিক কী সমর্থিত, তা প্রতিটির নিজের নথি আর version দেখে নিশ্চিত হতে হয়।
ঝুঁকি তিন ধরনের। এক, base-এর সঙ্গে মিল: adapter শুধু সেই base-এর সেই revision-এ অর্থপূর্ণ। Base আপডেট করলে সব adapter আবার শেখাতে বা অন্তত আবার মাপতে হয়। দুই, নিরাপত্তা আর বিশ্বাস: অন্যের দেওয়া adapter-ও একটি model; পর্ব ৫৩-এর মতো safetensors নিন আর উৎস যাচাই করুন। Adapter base model-এর আচরণ যেকোনো দিকে বদলাতে পারে, ক্ষতিকর দিকেও। তিন, পরিমাপ: শত adapter মানে শত model; প্রতিটির জন্য আলাদা test set আর regression test না থাকলে কোনটি খারাপ হয়েছে বোঝা যায় না। এর বিস্তারিত পর্ব ৫৮ · LLM মূল্যায়ন — benchmark, LLM-as-judge আর তার ফাঁদ-এ।
মূল উপলব্ধি: LoRA model-কে “একটি base + অনেক ছোট পরিবর্তন”-এ ভাগ করে। এতে জমা আর চালানো সস্তা হয়, কিন্তু সংস্করণ, উৎস আর মূল্যায়নের হিসাব রাখার দায়িত্ব বাড়ে।
প্র ০৫“LoRA r = ৩২ full fine-tuning-এর চেয়ে ভালো” — একটি split আর একটি seed-এর ফল থেকে এমন সিদ্ধান্ত কেন টানা যায় না? সৎভাবে তুলনা করবেন কীভাবে?
আমাদের টেবিলের পার্থক্যগুলো তৃতীয় দশমিক ঘরে। ২,১৪৩টি test মন্তব্যে macro-F1-এর ০.০০৫ পার্থক্য মানে হয়তো ডজনখানেক মন্তব্যের ভিন্ন সিদ্ধান্ত। একই config শুধু seed বদলালে, বা আরেকটি split নিলে, এর চেয়ে বড় ওঠানামা হতে পারে: পর্ব ৪৮-এ একই full fine-tuning-এর ফল ১৫টি split-এ ০.৯৩০ থেকে ০.৯৫৩ পর্যন্ত ঘুরেছিল। তাই একটি run দিয়ে ক্রম ঠিক করা মানে কয়েন একবার ছুড়ে সিদ্ধান্ত নেওয়া।
দ্বিতীয় সমস্যা hyperparameter-এর অসম যত্ন। আমরা full-এ ২ × ১০−৫ আর LoRA-তে ৫ × ১০−৪ দিয়েছি, কোনোটিই খোঁজা নয়, প্রচলিত শুরুর মান। যে পদ্ধতির lr বেশি খোঁজা হয়, সে সাধারণত এগিয়ে থাকে। গবেষণা প্রবন্ধেও এই সমস্যা আছে; তাই অনেক তুলনার ফল পরে অন্যরা পুনরুৎপাদন করতে গিয়ে বদলে যায়।
সৎ তুলনার নিয়ম কয়েকটি। এক, একাধিক split আর seed, আর গড়ের সঙ্গে ছড়ানো (standard deviation বা confidence interval). দুই, প্রতিটি পদ্ধতিকে একই পরিমাণ hyperparameter খোঁজা, একই validation set-এ। তিন, একই test সারিতে দুটি model-এর জোড়া তুলনা, যেমন McNemar test বা paired bootstrap; এর পরিসংখ্যান মেশিন লার্নিং কোর্সের পর্ব ২২-এ। চার, সরল baseline রাখা: এই একই split-এ TF-IDF + logistic regression ০.৯৪১ পেয়েছিল। পাঁচ, শুধু মান নয়, খরচও লেখা: parameter, memory, সময়, adapter-এর আকার। আর প্রতিটি সংখ্যার পাশে লিখুন “একটি স্থানীয় run”.
মূল উপলব্ধি: ছোট পার্থক্যের দাবি করতে বড় প্রমাণ লাগে। একটি run দেখায় কী সম্ভব; কোনটি ভালো, তা বলতে লাগে একাধিক seed, সমান যত্ন আর জোড়া পরিসংখ্যান।
অনুশীলন
খাতা-কলমে করুন, তারপর উত্তর মেলান। পরীক্ষা আর ইন্টারভিউতে এই ধরনের হিসাব কাজে লাগে।
-
হিসাব করুন: একটি ৭০০ কোটি (7 × 109) parameter-এর decoder-এ hidden আকার ৪০৯৬, ৩২টি layer, q আর v দুটিই ৪০৯৬ × ৪০৯৬। (ক) q আর v-তে r = ১৬ LoRA-র parameter কত? (খ) fp32 আর AdamW-তে full fine-tuning-এ activation বাদে memory? (গ) base bf16-এ, adapter fp32 + AdamW হলে LoRA-র memory?
(ক) এক module: ১৬ × (৪০৯৬ + ৪০৯৬) = ১,৩১,০৭২; দুটি module, ৩২টি layer: ১,৩১,০৭২ × ২ × ৩২ = ৮৩,৮৮,৬০৮ (প্রায় ৮৪ লাখ, মোটের ০.১২%). (খ) ১৬ byte × ৭ × ১০৯ = ১১২ GB. (গ) base: ২ × ৭ × ১০৯ = ১৪ GB; adapter: ৮৩,৮৮,৬০৮ × ১৬ ≈ ০.১৩৪ GB; মোট প্রায় ১৪.১৩ GB, activation বাদে। QLoRA-তে base প্রায় ৪.১২৭ bit/parameter হলে Linear অংশ আরও প্রায় চার ভাগের এক ভাগে নামে।
-
আউটপুট অনুমান করুন: কোড না চালিয়ে তিনটি লাইন লিখুন।
import numpy as np W = np.array([[1., 0.], [0., 2.]]) A = np.array([[1., -1.]]) B = np.zeros((2, 1)) x = np.array([3., 1.]) s = 4 / 1 print(W @ x + s * B @ (A @ x)) B = np.array([[0.5], [0.25]]) print(W @ x + s * B @ (A @ x)) print(np.linalg.matrix_rank(s * B @ A), (W + s * B @ A).shape)[3. 2.] [7. 4.] 1 (2, 2)Wx = [৩, ২]। B শূন্য, তাই প্রথম লাইন হুবহু Wx. Ax = ৩ − ১ = ২; B × ২ = [১, ০.৫]; × s = ৪ দিলে [৪, ২]; যোগ করে [৭, ৪]। BA একটি কলাম আর একটি সারির গুণফল, তাই rank ১; merge করা matrix-এর আকার W-এর মতোই (২, ২)।
-
নকশা সমালোচনা করুন: একটি দলের পরিকল্পনা: “Bangla BERT-এ সব Linear-এ LoRA r = ২৫৬, learning rate ২ × ১০−৫ (full-এর মতো), classifier head-এর আলাদা ব্যবস্থা নেই; training set-এ ৯৯% accuracy দেখে 4-bit base-এ merge করে চালু করব; পরে base-এর নতুন version এলে একই adapter লাগিয়ে দেব।” অন্তত পাঁচটি সমস্যা আর সমাধান বলুন।
- r = ২৫৬, ৭৬৮ মাপের layer-এ: r(d + k) = ২৫৬ × ১,৫৩৬ = ৩,৯৩,২১৬, মূল ৫,৮৯,৮২৪-এর দুই-তৃতীয়াংশ; LoRA-র সাশ্রয় প্রায় শেষ। সমাধান: ৪–৩২-এর মধ্যে কয়েকটি r মেপে বাছা।
- Full-এর learning rate: LoRA-তে প্রায়ই খুব ধীর। সমাধান: ছোট lr sweep (যেমন ১০−৪ থেকে ১০−৩), validation-এ মাপা।
- Head নেই: নতুন classifier এলোমেলো থেকে যাবে বা save হবে না। সমাধান:
task_type="SEQ_CLS"বাmodules_to_save. - Training set-এ মাপা: overfitting ধরা পড়ে না। সমাধান: আলাদা validation আর test, group-ভিত্তিক split, সরল baseline.
- 4-bit base-এ merge: rounding-এ output বদলায়। সমাধান: bf16 বা fp32 base-এ merge, আগে-পরে logits মেলানো।
- নতুন base-এ পুরনো adapter: ΔW পুরনো W-এর জন্য শেখা; নীরবে ভুল ফল। সমাধান: base-এর commit hash পিন, নতুন base-এ আবার শেখানো ও মাপা।
-
কোড লিখুন: একটি function
lora_count(shapes, targets, r, layers)লিখুন, যেখানেshapesহলো নাম থেকে (in, out)-এর dict. এটি মোট LoRA parameter ফেরত দেবে। Qwen2.5-0.5B-এর q (৮৯৬, ৮৯৬) আর v (৮৯৬, ১২৮), ২৪টি layer, r = ৮-এ ৫,৪০,৬৭২ আসে কি না যাচাই করুন।def lora_count(shapes, targets, r, layers): per_layer = sum(r * (shapes[t][0] + shapes[t][1]) for t in targets) return layers * per_layer shapes = {"q": (896, 896), "v": (896, 128)} assert lora_count(shapes, ["q", "v"], 8, 24) == 540_672প্রতিটি module-এ A (r × in) আর B (out × r), তাই r(in + out). ভুলের সাধারণ জায়গা: GQA-তে v-এর out মাপ hidden নয়, kv head × head মাপ (২ × ৬৪ = ১২৮)। বাস্তবে নিশ্চিত হতে peft-এর
print_trainable_parameters()-এর সঙ্গে মেলান, আর head-এর মতো পুরো শেখানো module আলাদা যোগ করুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি classifier-এ LoRA দিয়ে শেখানো থেকে চালু করা পর্যন্ত ধাপগুলো সাজান (▲▼ চেপে সরান)
- Pretrained model লোড করে সব ওজন frozen করা
- নির্বাচিত Linear-এ A (এলোমেলো) আর B (শূন্য) বসানো, head শেখার তালিকায় রাখা
- শুধু A, B আর head-কে AdamW দিয়ে শেখানো
- আলাদা validation আর test set-এ মান মাপা
- W + (α/r)BA দিয়ে merge করে আগে-পরে output মেলানো
একটি ১০২৪ × ১০২৪ Linear layer-এ r = ৮ LoRA বসালে কতটি নতুন শেখার parameter যোগ হয় (bias বাদে)?
- ৮,১৯২
- ১৬,৩৮৪
- ১০,৪৮,৫৭৬
- ৬৪
LoRA-তে B শূন্য আর A এলোমেলো মান দিয়ে শুরু করার মূল কারণ কোনটি?
- দুটি matrix শূন্যে শুরু করলে memory কম লাগে
- B শূন্য থাকলে adapter ফাইল ছোট হয়
- শুরুতে model হুবহু pretrained থাকে, তবু B gradient পেয়ে শিখতে পারে
- B শূন্য রাখলেই ΔW-এর rank r-এর নিচে থাকে
fp32 ওজন আর AdamW দিয়ে full fine-tuning-এ, activation বাদে, প্রতি parameter-এ মোটামুটি কত byte লাগে?
- ১৬ byte
- ৪ byte
- ৮ byte
- ৩২ byte
একটি LoRA adapter W′ = W + (α/r)BA দিয়ে merge করার পর inference-এ কী ঘটে?
- প্রতিটি layer-এ দুটি গুণ চলে, তাই latency সামান্য বাড়ে
- Model-এর parameter r(d + k) বেড়ে যায়
- Output বদলে যায়, তাই আবার training লাগে
- একটিই matrix গুণ হয়, বাড়তি latency থাকে না, output একই
QLoRA-র “double quantization” আসলে কী quantize করে?
- LoRA-র A আর B matrix-কে 4-bit-এ
- ওজনের block-গুলোর quantization scale ধ্রুবকগুলো
- ওজনকে প্রথমে 8-bit, তারপর 4-bit-এ
- Optimizer-এর m আর v-কে CPU-তে পাঠানো
১৬ কোটির একটি BERT-এ full fine-tuning-এর peak GPU memory ছিল প্রায় ৩,৯৭০ MiB, আর LoRA r = ৮-এ প্রায় ১,৭৭০ MiB, যদিও শেখার parameter ৫০০ গুণের বেশি কম। Memory আরও বেশি না কমার প্রধান কারণ কোনটি?
- LoRA-তেও সব ওজনের gradient জমা থাকে
- Adam LoRA-তে বেশি state রাখে
- Frozen ওজন আর activation memory-তে থেকেই যায়
- r = ৮ খুব বড়, r = ১ নিলে memory ১০ গুণ কমত
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Full fine-tuning-এ ১৬ byte/parameter (fp32 ওজন, gradient, Adam-এর m আর v), activation আলাদা।
- LoRA: h = W0x + (α/r)BAx; W0 frozen, প্রতি module-এ r(d + k)টি নতুন parameter.
- A এলোমেলো, B = 0: শুরুতে model হুবহু আগের; α/r rank বদলালেও মাপ স্থির রাখে।
- Merge করলে একটিই matrix, বাড়তি latency নেই; অনেক কাজে merge না করে adapter বদলান, আর base-এর hash লিখে রাখুন।
- QLoRA = NF4 base + double quantization + paged optimizer; মান, memory আর সময় নিজের কাজে মেপে সিদ্ধান্ত নিন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৬ · Instruction tuning — SFT ডেটা, chat template আর ফরম্যাটপরবর্তী পর্বLoRA দিয়ে SFT, শুধু উত্তরের token-এ loss.
- ডিপ লার্নিং, NLP আর Generative AI · কোর্স হোম৯১টি পর্বপুরো কোর্সের রোডম্যাপ।
- পর্ব ৪৮ · BERT পরিবারফিরে দেখুনএকই ডেটায় full fine-tuning, head-only আর LLRD.
- মেশিন লার্নিং · পর্ব ৯ · SVDপাশের ট্র্যাকLow-rank approximation আর Eckart–Young.
- মেশিন লার্নিং · পর্ব ১৪ · Momentum, RMSProp আর Adamপাশের ট্র্যাকAdam-এর m আর v কোথা থেকে আসে।