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

GPU আর AI accelerator — হাজার core, SIMT আর tensor unit

GPUs and AI Accelerators — Throughput Design, SIMT Warps, Memory Coalescing, Tensor Cores and Systolic Arrays

Taught in Bangla, this lesson covers GPUs and AI accelerators: latency versus throughput design, kernels, grids, blocks and threads, SIMT warps with branch divergence and reconvergence, occupancy, the GPU memory hierarchy, coalescing and bank conflicts, vector-add and tiled matrix-multiply kernels, PCIe offload break-even, tensor cores, systolic arrays and low precision, TOPS, the bandwidth limit of AI inference, and GPU versus FPGA versus ASIC.

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

  • CPU আর GPU-র নকশার দর্শন, আর GPU-র ইতিহাস — fixed-function graphics থেকে CUDA আর AI
  • Kernel, grid, thread block, warp; SIMT-এ active mask আর reconvergence stack — হাতে আর simulator-এ
  • Warp বদলে latency লুকানো আর occupancy-র হিসাব; GPU-র মেমরি হায়ারার্কি, coalescing আর bank conflict
  • Vector-add আর tiled matrix multiply kernel লাইন ধরে; PCIe-র খরচ আর offload-এর break-even
  • Tensor core, systolic array, কম precision, TOPS, NPU আর AI inference-এর bandwidth সীমা; GPU বনাম FPGA বনাম ASIC

১ · CPU বনাম GPU — একটি কাজ দ্রুত, নাকি অনেক কাজ একসাথে

পুরান ঢাকার একটি বিয়েবাড়ির রান্নার কথা ভাবুন। এক দিকে একজন নামী বাবুর্চি — তিনি একাই একটি জটিল পদ খুব দ্রুত রাঁধেন, কোন মশলা কখন লাগবে আগে থেকেই আন্দাজ করেন, হাতের কাছে সব উপকরণ গুছিয়ে রাখেন। অন্য দিকে পাঁচশো জন সহকারী — প্রত্যেকে সাধারণ কাজ পারেন: পেঁয়াজ কাটা, আলু ছোলা। একজন সহকারী বাবুর্চির চেয়ে অনেক ধীর, কিন্তু পাঁচশো জন একসাথে পাঁচশো কেজি পেঁয়াজ কাটলে মোট কাজের হার বাবুর্চির ধারেকাছেও কেউ যেতে পারবে না। আর কারো হাতে পেঁয়াজ ফুরালে তিনি বসে থাকেন না — পাশের ঝুড়ি থেকে আলু তুলে নেন।

CPU সেই বাবুর্চি। তার লক্ষ্য একটি thread-এর latency কমানো — অর্থাৎ একটি কাজ যত দ্রুত সম্ভব শেষ করা। এজন্য প্রতিটি core-এ বিশাল যন্ত্রপাতি: branch predictor (পর্ব ৩৭), out-of-order engine আর reorder buffer (পর্ব ৪০), আর মেমরির অপেক্ষা কমাতে বড় বড় cache (পর্ব ৪৪)। GPU সেই সহকারীর দল। তার লক্ষ্য throughput — প্রতি সেকেন্ডে মোট কত কাজ। প্রতিটি lane সরল, in-order, predictor নেই বললেই চলে; বাঁচানো জায়গায় বসানো হয় আরও অনেক ALU. আর মেমরির অপেক্ষা সে কমায় না, ঢেকে দেয় — একদল thread অপেক্ষায় থাকলে আরেক দল চলে।

CPU — কয়েকটি বড় core GPU — অনেক ছোট lane Control,OoO, predictor Control,OoO, predictor Control,OoO, predictor Control,OoO, predictor বড় cache (L2/L3) latency কমানোর জন্য L2 cache (তুলনায় ছোট) ■ control ■ ALU (হিসাবের unit) ■ cache ধারণাগত অনুপাত — আসল chip-এর মাপ নয়
একই আকারের silicon কোথায় খরচ হয় — ধারণাগত ছবি। CPU-র জায়গার বড় অংশ যায় control (অনুমান, পুনর্বিন্যাস) আর cache-এ, যাতে একটি thread দ্রুত চলে। GPU সেই জায়গায় শত শত ALU বসায়, control-কে অনেক lane-এর মধ্যে ভাগ করে নেয়, আর অপেক্ষা ঢাকে thread বদলে।
দিকCPU (latency-কেন্দ্রিক)GPU (throughput-কেন্দ্রিক)
Coreকয়েকটি থেকে কয়েক ডজন বড় coreকয়েক ডজন থেকে শতাধিক SM, মোট হাজার হাজার সরল lane
একটি thread-এর গতিখুব দ্রুত — উঁচু clock, OoO, বড় predictorধীর — in-order, সরল
মেমরির অপেক্ষাবড় cache আর prefetch দিয়ে কমায়হাজার thread পালা করে চালিয়ে ঢাকে
একসাথে জীবিত threadপ্রতি core-এ ১–২টি (SMT, পর্ব ৪৩)প্রতি SM-এ এক-দুই হাজার পর্যন্ত
মেমরিDDR — বড় capacity, মাঝারি bandwidthGDDR বা HBM — অনেক বেশি bandwidth (পর্ব ৪৯)
ভালো কাজশাখা-ভরা, নির্ভরশীল, অনিয়মিত কোড; OS, browser, databaseএকই হিসাব লাখো স্বাধীন ডেটায় — ছবি, ভিডিও, simulation, matrix, AI
মূল ধারণা

Little-এর সূত্রের ভাষায় — মেমরি থেকে প্রতি cycle-এ B byte টানতে চাইলে আর প্রতিটি অনুরোধে L cycle লাগলে, একসাথে প্রায় B × L byte-এর অনুরোধ “পথে” থাকতে হবে। CPU L কমিয়ে কাজ চালায়; GPU L মেনে নিয়ে হাজার হাজার thread দিয়ে একসাথে অনেক অনুরোধ পথে রাখে। পর্ব ৪৩-এর fine-grained multithreading-ই এখানে বিশাল আকারে।

২ · Graphics থেকে সাধারণ হিসাবে — ইতিহাস আর GPU-র execution model

সংক্ষিপ্ত ইতিহাস (বছরগুলো মোটামুটি): আশি-নব্বইয়ের দশকের graphics card ছিল fixed-function — ত্রিভুজ আঁকা, texture বসানোর মতো নির্দিষ্ট কাজের জন্য তার দিয়ে বাঁধা circuit, প্রোগ্রাম করা যেত না। ১৯৯৯ সালে NVIDIA তাদের GeForce 256-কে “GPU” নামে বাজারে আনে। ২০০১-এর আশেপাশে আসে প্রোগ্রাম করা যায় এমন shader — প্রতিটি pixel বা vertex-এর জন্য চালানো ছোট প্রোগ্রাম। গবেষকেরা তখন ছবির ছদ্মবেশে সাধারণ হিসাব (matrix, physics) GPU-তে চালাতে শুরু করেন — একে বলে GPGPU. ২০০৬-০৭ সালে NVIDIA একীভূত (unified) shader নকশা আর CUDA আনে — C-এর মতো ভাষায় সরাসরি GPU প্রোগ্রাম। প্রায় একই সময়ে (২০০৮-০৯) Khronos Group-এর খোলা মান OpenCL আসে, যা বিভিন্ন কোম্পানির GPU আর অন্য accelerator-এ চলে। ২০১২ সালে AlexNet নামের একটি neural network দুটি gaming GPU-তে train করে ছবি চেনার প্রতিযোগিতায় বড় ব্যবধানে জেতে — deep learning-এর জোয়ার সেখান থেকে।

Kernel, grid, block, thread

GPU-তে চালানো function-কে বলে kernel. CPU (যাকে বলা হয় host) একটি kernel চালু করার সময় বলে দেয় কতগুলো thread লাগবে, আর GPU (device) সেই kernel-এর একই কোড প্রতিটি thread-এ চালায় — প্রতিটি thread শুধু নিজের নম্বর দেখে ঠিক করে কোন ডেটায় কাজ করবে। Thread-গুলো দুই স্তরে সাজানো:

  • Grid: একটি kernel-এর সব thread মিলে একটি grid. Grid অনেকগুলো block-এ ভাগ।
  • Thread block (OpenCL-এ workgroup): একসাথে একটি SM-এ বসে এমন thread-এর দল, সাধারণত ১২৮–১০২৪টি। একই block-এর thread-রা দ্রুত on-chip shared memory ভাগ করে আর একটি barrier-এ (__syncthreads()) একে অপরের জন্য অপেক্ষা করতে পারে; আলাদা block-এর thread-রা পারে না।
  • Thread: প্রতিটির নিজস্ব register আর নম্বর (threadIdx, blockIdx). Hardware এদের ৩২টি করে warp-এ দলবদ্ধ করে চালায় (অংশ ৩)।

গার্মেন্টস কারখানার সাথে মিলিয়ে দেখুন: পুরো অর্ডার (১০ লাখ শার্ট) হলো grid; প্রতিটি line-কে দেওয়া একটি লট হলো block — এক line-এর অপারেটররা একই টেবিলের সরঞ্জাম (shared memory) ভাগ করেন আর “সবাই এই ধাপ শেষ করলে পরের ধাপ” বলে থামতে পারেন; একজন অপারেটর একটি thread; আর পাশাপাশি বসা ৩২ জন, যাঁরা সুপারভাইজারের একটি বাঁশিতে একই সেলাই একসাথে দেন — একটি warp.

সবচেয়ে সরল kernel — দুটি array যোগ। নিচের CUDA কোড চালানোর জন্য নয় (এই পাতায় GPU নেই), লাইন ধরে বোঝার জন্য:

// device code: one thread adds one element
__global__ void vadd(const float *a, const float *b, float *c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;   // global thread index
    if (i < n) c[i] = a[i] + b[i];                   // guard: last block overhangs
}

// host code (error checks omitted for brevity)
void add_on_gpu(const float *a, const float *b, float *c, int n) {
    size_t bytes = n * sizeof(float);
    float *da, *db, *dc;
    cudaMalloc(&da, bytes); cudaMalloc(&db, bytes); cudaMalloc(&dc, bytes);
    cudaMemcpy(da, a, bytes, cudaMemcpyHostToDevice);   // over PCIe
    cudaMemcpy(db, b, bytes, cudaMemcpyHostToDevice);
    int threads = 256;
    int blocks = (n + threads - 1) / threads;           // ceil(n / 256)
    vadd<<<blocks, threads>>>(da, db, dc, n);            // launch the grid
    cudaMemcpy(c, dc, bytes, cudaMemcpyDeviceToHost);   // waits for the kernel
    cudaFree(da); cudaFree(db); cudaFree(dc);
}

লাইন ধরে: __global__ বলে এটি GPU-তে চলবে কিন্তু host থেকে ডাকা হবে। i = blockIdx.x * blockDim.x + threadIdx.x — “আমি কোন block-এর, block-এ কততম” থেকে নিজের বৈশ্বিক নম্বর; ২৫৬ thread-এর block-এ block 2-এর thread 5 পায় i = 2 × 256 + 5 = 517. if (i < n) পাহারা দরকার কারণ thread-সংখ্যা block-এর গুণিতক — n = ১০,০০,০০০ হলে block = ⌈১০,০০,০০০ / ২৫৬⌉ = ৩,৯০৭টি, মোট thread ১০,০০,১৯২টি, শেষ ১৯২টি কিছু করে না। Host দিকে cudaMalloc GPU-র মেমরিতে জায়গা নেয়, cudaMemcpy ডেটা PCIe দিয়ে পাঠায় (অংশ ৬-এ এর দাম), <<<blocks, threads>>> kernel চালু করে, তারপর ফল ফেরত আনা আর মেমরি ছাড়া। লক্ষ করুন, kernel-এ কোনো লুপ নেই — লুপটি “ছড়িয়ে” গেছে ১০ লাখ thread-এ।

৩ · SIMT আর warp — একটি instruction, অনেক thread, আর branch divergence

প্রোগ্রামারের চোখে প্রতিটি thread স্বাধীন — নিজের PC, নিজের if. কিন্তু hardware-এ ৩২টি thread-এর একটি warp একটি instruction fetch-decode করে আর ৩২টি lane-এ একসাথে চালায়, প্রতিটি lane নিজের register নিয়ে। NVIDIA এই ধরনকে বলে SIMT (single instruction, multiple threads). এটি পর্ব ৪২-এর SIMD-এরই আত্মীয় — পার্থক্য হলো, SIMD-এ প্রোগ্রামার বা compiler নিজে lane আর mask সামলায়, আর SIMT-এ প্রতিটি lane-কে আলাদা thread হিসেবে লেখা হয়, mask সামলায় hardware. Warp-এর আকার নির্মাতাভেদে আলাদা: NVIDIA-র warp ৩২ thread; AMD-এর পুরোনো GCN নকশায় wavefront ৬৪, নতুন RDNA নকশায় ৩২ বা ৬৪ — প্রকাশিত বিবরণ অনুযায়ী।

সমস্যা বাধে শাখায়। Warp-এর কিছু thread if-এর ভেতরে যেতে চায়, কিছু else-এ — অথচ instruction একটিই। সমাধান: hardware একটি active mask রাখে (প্রতি lane-এ এক bit, পর্ব ৪২-এর vector mask-এর মতো)। প্রথমে if-পথ চলে শুধু সেই lane-গুলোতে, বাকিরা চুপ; তারপর else-পথ উল্টো mask-এ; শেষে দুই পথ যেখানে মেলে (reconvergence point), সেখানে আবার সবাই একসাথে। কোথায় মিলবে আর কোন পথ বাকি, তা মনে রাখতে একটি reconvergence stack — ক্লাসিক নকশায় প্রতিটি entry-তে (PC, mask, মেলার PC). একে বলে branch divergence. নিচে ৮ lane-এর একটি ছোট warp (বোঝার সুবিধায়; আসলে ৩২):

x = [4, 7, 2, 9, 6, 3, 8, 5]; mask-এ বাঁ দিকের bit = lane 0
এই ধাপের instruction
Active mask
কাজ করা lane
y (lane 0 থেকে 7)
Reconvergence stack (ওপরেরটি চলছে)
দক্ষতা (এ পর্যন্ত)
  1. Load — সবাই একসাথে। একটি load instruction আটটি lane-এ: lane t পড়ে x[t]. Mask 11111111, আটটির আটটিই কাজে।
  2. তুলনা আর শাখা। সবাই তুলনা করে। জোড় x আছে lane 0, 2, 4, 6-এ (4, 2, 6, 8), বিজোড় lane 1, 3, 5, 7-এ — warp বিভক্ত। Hardware stack-এ তিনটি জিনিস রাখে: মেলার জায়গা (পুরো mask), else-পথ (01010101) আর ওপরে then-পথ (10101010).
  3. Then-পথ। Mask 10101010: শুধু জোড় lane-এ y = y / 2 — 4→2, 2→1, 6→3, 8→4. বিজোড় lane-গুলো এই cycle-এ বসে থাকে। Instruction একটি issue হলো, কাজ হলো ৪টি lane-এ।
  4. Else-পথ, প্রথম instruction. Then শেষ, stack থেকে সেটি সরে; এবার mask 01010101: y = 3 × y — 7→21, 9→27, 3→9, 5→15. এবার জোড় lane-রা অলস।
  5. Else-পথ, দ্বিতীয় instruction. y = y + 1 → 22, 28, 10, 16. Else-পথ দুটি instruction লম্বা, তাই অর্ধেক-ফাঁকা warp দুই cycle কাটায়।
  6. Reconvergence. দুই পথ মিলেছে; stack-এর নিচের পুরো mask আবার চালু, store সবাই একসাথে করে। মোট ৬টি instruction issue, lane-slot ৬ × ৮ = ৪৮, কাজে লেগেছে ৮ + ৮ + ৪ + ৪ + ৪ + ৮ = ৩৬ — SIMD দক্ষতা ৩৬ / ৪৮ = ৭৫%. Divergence না থাকলে (সব x জোড়) ৪টি instruction-এ কাজ শেষ হতো।
সূত্র

SIMD দক্ষতা = (সব issue-করা instruction-এ সক্রিয় lane-এর যোগফল) / (issue-করা instruction × warp-এর আকার)। Divergent if/else-এ warp দুই পথের instruction-সংখ্যার যোগফল সময় নেয়, যেকোনো একটির নয়। একটি পরীক্ষার ধাঁচের হিসাব: ৩২-thread warp; শাখার আগে ২টি আর পরে ২টি সাধারণ instruction; if (tid < 24) পথে ৮টি, else-পথে ৪টি instruction. Issue = ২ + ৮ + ৪ + ২ = ১৬. সক্রিয় lane-slot = ২×৩২ + ৮×২৪ + ৪×৮ + ২×৩২ = ৬৪ + ১৯২ + ৩২ + ৬৪ = ৩৫২. দক্ষতা = ৩৫২ / (১৬ × ৩২) = ৩৫২ / ৫১২ = ৬৮.৭৫%, আর সময় ১৬ cycle-এর সমান issue — সবাই if-পথে গেলে লাগত ১২.

লুপেও একই ঘটনা: প্রতিটি thread-এর ঘোরার সংখ্যা আলাদা হলে যে thread আগে শেষ, তার lane বসে থাকে, যতক্ষণ না warp-এর সবচেয়ে ধীর thread শেষ করে। নিচের simulator ৮ lane-এর একটি warp-এ একটি if/else আর একটি লুপ (প্রতিটি thread-এর আলাদা trip count) চালায় — প্রতিটি issue-তে PC, instruction, mask, সক্রিয় lane আর stack-এর গভীরতা ছাপে। Run চাপুন:

// SIMT warp of 8 threads: active mask + reconvergence stack
const W = 8, trip = [2, 0, 1, 3, 0, 2, 1, 0];
const v = new Array(W).fill(0), n = new Array(W).fill(0);
const P = [ // br: lanes where cond is true jump to tgt; rpc = reconvergence pc
  { s: "v = t; n = trip[t]", f: t => { v[t] = t; n[t] = trip[t]; } },
  { s: "if (t % 2) goto 4", br: t => t % 2 === 1, tgt: 4, rpc: 6 },
  { s: "v = v * 10   (even)", f: t => { v[t] *= 10; } },
  { s: "goto 6", br: () => true, tgt: 6, rpc: 6 },
  { s: "v = v + 100  (odd)", f: t => { v[t] += 100; } },
  { s: "v = v - 1    (odd)", f: t => { v[t] -= 1; } },
  { s: "if (n == 0) goto 9", br: t => n[t] === 0, tgt: 9, rpc: 9 },
  { s: "v = v + 1; n = n - 1", f: t => { v[t] += 1; n[t] -= 1; } },
  { s: "goto 6", br: () => true, tgt: 6, rpc: 9 },
  { s: "out[t] = v", f: () => {} }];
const stack = [{ pc: 0, mask: (1 << W) - 1, rpc: -1 }];
const bits = m => [...Array(W)].map((_, t) => (m >> t) & 1).join("");
let issued = 0, busy = 0;
while (stack.length) {
  const top = stack[stack.length - 1];
  if (top.pc === top.rpc || top.pc >= P.length) { stack.pop(); continue; }
  const pc = top.pc, op = P[pc], lanes = [...Array(W).keys()].filter(t => (top.mask >> t) & 1);
  const act = lanes.length; issued++; busy += act;
  console.log(String(pc).padStart(2) + " " + op.s.padEnd(21) + bits(top.mask) + " " + act + "/" + W + " depth " + stack.length);
  if (!op.br) { lanes.forEach(op.f); top.pc++; continue; }
  let T = 0; lanes.forEach(t => { if (op.br(t)) T |= 1 << t; });
  const N = top.mask & ~T;
  if (!N) top.pc = op.tgt; else if (!T) top.pc++;
  else if (top.rpc === op.rpc) { top.mask = N; top.pc++; }   // taken lanes just wait at rpc
  else { top.pc = op.rpc;                                     // this entry = reconvergence point
    stack.push({ pc: op.tgt, mask: T, rpc: op.rpc }, { pc: pc + 1, mask: N, rpc: op.rpc }); }
}
console.log("out  = [" + v.join(", ") + "]");
console.log("SIMD efficiency = " + busy + " / (" + issued + " x " + W + ") = " + (100 * busy / (issued * W)).toFixed(1) + "%");

আউটপুট (নির্ধারিত — প্রতিবার একই):

 0 v = t; n = trip[t]   11111111 8/8 depth 1
 1 if (t % 2) goto 4    11111111 8/8 depth 1
 2 v = v * 10   (even)  10101010 4/8 depth 3
 3 goto 6               10101010 4/8 depth 3
 4 v = v + 100  (odd)   01010101 4/8 depth 2
 5 v = v - 1    (odd)   01010101 4/8 depth 2
 6 if (n == 0) goto 9   11111111 8/8 depth 1
 7 v = v + 1; n = n - 1 10110110 5/8 depth 3
 8 goto 6               10110110 5/8 depth 3
 6 if (n == 0) goto 9   10110110 5/8 depth 3
 7 v = v + 1; n = n - 1 10010100 3/8 depth 3
 8 goto 6               10010100 3/8 depth 3
 6 if (n == 0) goto 9   10010100 3/8 depth 3
 7 v = v + 1; n = n - 1 00010000 1/8 depth 3
 8 goto 6               00010000 1/8 depth 3
 6 if (n == 0) goto 9   00010000 1/8 depth 3
 9 out[t] = v           11111111 8/8 depth 1
out  = [2, 100, 21, 105, 40, 106, 61, 106]
SIMD efficiency = 75 / (17 x 8) = 55.1%

পড়ার নিয়ম: প্রথম কলাম PC, তারপর instruction, তারপর mask (বাঁ দিকে lane 0), সক্রিয় lane আর stack-এর গভীরতা। PC 1-এ warp ভাগ হয় — জোড় lane (10101010) আগে PC 2–3 চালায়, stack গভীরতা ৩; তারপর বিজোড় lane (01010101) PC 4–5; PC 6-এ আবার 11111111. লুপে প্রথম পরীক্ষায় trip = 0-এর lane 1, 4, 7 বেরিয়ে যায়, mask হয় 10110110 (৫টি); দ্বিতীয় ঘোরার পর 10010100 (lane 0, 3, 5 বাকি — trip ২ বা ৩), তৃতীয় ঘোরায় শুধু lane 3 (00010000, ১/৮) — বাকি সাতটি lane শুধু অপেক্ষা করছে। ফল out ঠিক: যেমন lane 3-এ 3 + 100 − 1 + 3 = 105. মোট ১৭টি issue-তে ৭৫টি lane-slot কাজে লেগেছে: দক্ষতা ৫৫.১%. সব trip = 1 করে চালালে ১১টি issue-তে ৭২ — ৮১.৮% (অনুশীলন ১)। Code-এ top.rpc === op.rpc লাইনটি লুপের জন্য একটি সাধারণ কৌশল: যে lane লুপ ছাড়ে সে নতুন entry না বানিয়ে শুধু mask থেকে বাদ পড়ে আর মেলার জায়গায় অপেক্ষা করে।

বাস্তবে কী হয়: খুব ছোট শাখায় compiler প্রায়ই branch না বসিয়ে predication ব্যবহার করে — দুই পথই চালিয়ে mask দিয়ে ফল বাছে। আর NVIDIA-র Volta (২০১৭) থেকে প্রতিটি thread-এর নিজের PC রাখা হয় বলে প্রকাশিত হয়েছে (“independent thread scheduling”) — divergent পথগুলো আরও নমনীয়ভাবে পালা করে চলতে পারে, তবে একই সময়ে একটি warp তবুও একটি পথই চালায়, তাই দক্ষতার হিসাব একই থাকে। প্রোগ্রামারের শিক্ষা: শর্ত যেন warp ধরে একরকম হয় — যেমন if (tid / 32 % 2) পুরো warp-কে একই দিকে পাঠায়, divergence-ই হয় না।

৪ · Warp বদলে অপেক্ষা লুকানো — occupancy-র হিসাব

GPU-র global memory থেকে একটি load-এ কয়েকশো cycle লাগতে পারে। একটি warp load চালিয়ে ডেটার অপেক্ষায় থাকলে SM-এর warp scheduler পরের cycle-এই আরেকটি প্রস্তুত warp-এর instruction issue করে। এটি পর্ব ৪৩-এর fine-grained multithreading, কিন্তু context switch একেবারে বিনা খরচে — কারণ প্রতিটি জীবিত warp-এর register আগে থেকেই SM-এর বিশাল register file-এ বসানো, কিছু সরাতে হয় না। একে বলে latency hiding. সরল হিসাব: একটি warp প্রতি ৪টি হিসাবের instruction-এর পর একটি load দেয়, যার latency ৪০০ cycle; SM প্রতি cycle-এ একটি warp-instruction issue করলে, ৪০০ cycle ব্যস্ত রাখতে মোটামুটি ৪০০ / ৪ = ১০০টি warp-এর কাজ লাগবে — অথবা প্রতিটি warp-এর ভেতরে কয়েকটি স্বাধীন load, যাতে কম warp-এ চলে।

কিন্তু SM-এ একসাথে কতগুলো warp বসতে পারবে, তা সীমিত তিনটি জিনিসে: register file-এর মাপ, shared memory-র মাপ, আর thread/block-এর সর্বোচ্চ সংখ্যা। জীবিত warp / সর্বোচ্চ সম্ভব warp — এই অনুপাতের নাম occupancy. নিচের হিসাবগুলোতে SM-এর সীমা শেখার জন্য ধরে নেওয়া — ৬৫,৫৩৬টি 32-bit register, ৬৪ KB shared memory, সর্বোচ্চ ২,০৪৮ thread (৬৪টি warp) আর ৩২টি block. (অনেক বাস্তব GPU-র সংখ্যা এর কাছাকাছি, কিন্তু প্রজন্মভেদে আলাদা, আর hardware register বরাদ্দ করে নির্দিষ্ট খণ্ডে — তাই আসল হিসাব নির্মাতার occupancy calculator-এ মেলাতে হয়।)

হাতে হিসাব

Kernel: block-এ ২৫৬ thread (৮টি warp), প্রতি thread ৬৪টি register, প্রতি block ১৬ KB shared memory.
Register-এর সীমা: একটি block-এর দরকার ২৫৬ × ৬৪ = ১৬,৩৮৪টি register → ৬৫,৫৩৬ / ১৬,৩৮৪ = ৪টি block.
Shared memory-র সীমা: ৬৪ / ১৬ = ৪টি block. Thread-এর সীমা: ২,০৪৮ / ২৫৬ = ৮টি block.
সবচেয়ে ছোটটি জেতে: ৪টি block = ৩২টি warp → occupancy = ৩২ / ৬৪ = ৫০%. Register ৩২-এ নামালেও লাভ নেই — shared memory এখনও ৪টিতে আটকায়; দুটোই কমালে (৩২ register, ৮ KB) ৮টি block, ১০০%.

নিচের calculator একই হিসাব কয়েকটি kernel-এর জন্য করে আর বলে কোন সম্পদ সীমা টানছে:

// Occupancy calculator. SM limits below are ASSUMED teaching values,
// not the datasheet of any real GPU (real GPUs also round allocations up).
const SM = { regs: 65536, smem: 64 * 1024, maxThreads: 2048, maxBlocks: 32, warp: 32 };
const maxWarps = SM.maxThreads / SM.warp;
function occupancy(threadsPerBlock, regsPerThread, smemPerBlock) {
  const byRegs = Math.floor(SM.regs / (threadsPerBlock * regsPerThread));
  const bySmem = smemPerBlock ? Math.floor(SM.smem / smemPerBlock) : Infinity;
  const byThreads = Math.floor(SM.maxThreads / threadsPerBlock);
  const limits = { registers: byRegs, "shared mem": bySmem, threads: byThreads, "block cap": SM.maxBlocks };
  const blocks = Math.min(...Object.values(limits));
  const who = Object.keys(limits).filter(k => limits[k] === blocks).join(" + ");
  const warps = blocks * threadsPerBlock / SM.warp;
  return { byRegs, bySmem, byThreads, blocks, warps, who, occ: (100 * warps / maxWarps).toFixed(1) };
}
console.log("max resident warps per SM = " + maxWarps);
console.log("block regs/thr smem(KB) | regs smem thr -> blocks warps  occ   limiter");
const cases = [[256, 64, 16], [256, 32, 16], [256, 32, 8], [256, 40, 0], [128, 128, 0], [1024, 32, 48]];
for (const [tpb, r, kb] of cases) {
  const o = occupancy(tpb, r, kb * 1024);
  const sm = o.bySmem === Infinity ? "-" : o.bySmem;
  console.log(String(tpb).padStart(5) + String(r).padStart(9) + String(kb).padStart(9) + "   |" +
    String(o.byRegs).padStart(5) + String(sm).padStart(5) + String(o.byThreads).padStart(4) + " ->" +
    String(o.blocks).padStart(6) + String(o.warps).padStart(6) + (o.occ + "%").padStart(7) + "  " + o.who);
}

আউটপুট (ধরে নেওয়া SM-সীমায়):

max resident warps per SM = 64
block regs/thr smem(KB) | regs smem thr -> blocks warps  occ   limiter
  256       64       16   |    4    4   8 ->     4    32  50.0%  registers + shared mem
  256       32       16   |    8    4   8 ->     4    32  50.0%  shared mem
  256       32        8   |    8    8   8 ->     8    64 100.0%  registers + shared mem + threads
  256       40        0   |    6    -   8 ->     6    48  75.0%  registers
  128      128        0   |    4    -  16 ->     4    16  25.0%  registers
 1024       32       48   |    2    1   2 ->     1    32  50.0%  shared mem

প্রথম সারি ওপরের হাতের হিসাব — ৪টি block, ৩২টি warp, ৫০%, register আর shared memory দুটিই সীমায়। দ্বিতীয় সারিতে register অর্ধেক করলেও ৫০%, কারণ এবার একা shared memory আটকায়। তৃতীয়তে দুটোই কমানোয় ১০০%. চতুর্থ: ৪০টি register মানে ২৫৬ × ৪০ = ১০,২৪০, আর ৬৫,৫৩৬ / ১০,২৪০ = ৬.৪ → ৬টি block, ৭৫%. পঞ্চম: প্রতি thread ১২৮টি register-এর ভারী kernel মাত্র ২৫%. শেষ সারি: ৪৮ KB shared memory-র একটি বড় block একাই SM ভরায় — ১টি block, ৩২টি warp. মনে রাখবেন, occupancy বেশি মানেই দ্রুত নয়: কম warp কিন্তু প্রতিটিতে বেশি স্বাধীন কাজ (instruction-level parallelism) আর বেশি register-এ ডেটা রাখা অনেক সময় ভালো চলে। Occupancy শুধু বলে অপেক্ষা ঢাকার জন্য হাতে কতগুলো warp আছে।

৫ · GPU-র মেমরি — register, shared memory, coalescing আর bank conflict

GPU-র মেমরি হায়ারার্কি CPU-র মতোই স্তরে স্তরে (পর্ব ৪৪), কিন্তু ভাগাভাগি আলাদা:

স্তরকে দেখেবৈশিষ্ট্য
Registerএকটি threadসবচেয়ে দ্রুত; SM-এর register file বিশাল (প্রায়শই শত KB), হাজার thread-এ ভাগ
Shared memoryএকটি block-এর সব threadSM-এর ভেতরে, প্রোগ্রামার নিজে কী রাখবে ঠিক করেন — একটি scratchpad memory; অনেক GPU-তে L1 cache-এর সাথে একই SRAM ভাগ করা
L1 / L2 cacheSM / পুরো GPUHardware-চালিত; CPU-র তুলনায় thread-প্রতি অনেক ছোট
Global memoryসব thread আর hostবাইরের GDDR বা HBM chip; বিশাল bandwidth কিন্তু কয়েকশো cycle latency (পর্ব ৪৯)

Memory coalescing — পাশাপাশি thread, পাশাপাশি ঠিকানা

একটি warp-এর ৩২টি thread একটি load instruction একসাথে চালায়, তাই ৩২টি ঠিকানা একসাথে আসে। Memory system সেগুলো মিলিয়ে দেখে কয়টি আলাদা মেমরি-খণ্ড (segment) ছুঁতে হবে, আর প্রতিটি খণ্ডের জন্য একটি transaction পাঠায়। ঠিকানাগুলো পাশাপাশি হলে সব একটি বা দুটি খণ্ডে পড়ে — একে বলে memory coalescing. ছড়ানো হলে (strided access) প্রায় প্রতিটি thread-এর জন্য আলাদা transaction, আর আনা ডেটার বেশিরভাগ অপচয়। নিচের হিসাবে পাঠ্যবইয়ের মডেল ধরে নেওয়া: ৩২ thread, প্রতিটি 4-byte উপাদান, thread t পড়ে base + t × stride × 4, আর মেমরি ১২৮-byte খণ্ডে আসে (base 0 মানে খণ্ডের শুরুতে align করা):

৩২ thread × 4 byte = ১২৮ byte দরকারি ডেটা; খণ্ড = ১২৮ byte
Access-এর ধরন
Thread 0–3-এর ঠিকানা
Thread 31-এর ঠিকানা
ছোঁয়া খণ্ড
Transaction / আনা byte
দক্ষতা
  1. Stride 1 — আদর্শ। Thread t পড়ে ঠিকানা 4t: 0 থেকে 124 পর্যন্ত, সব একটি ১২৮-byte খণ্ডে। একটি transaction, যা আনা হলো তার সবটাই কাজে — দক্ষতা ১০০%.
  2. Stride 2. প্রতিটি thread একটি উপাদান বাদ দিয়ে পড়ে: ঠিকানা 0, 8, … 248 — দুটি খণ্ড। ২৫৬ byte আনা হলো, কাজে লাগল ১২৮ — ৫০%.
  3. Stride 4. ঠিকানা 0 থেকে 496, চারটি খণ্ড — ৫১২ byte এনে ১২৮ কাজে, ২৫%.
  4. Stride 32 — সবচেয়ে খারাপ। পাশাপাশি thread-এর ঠিকানার ব্যবধান ১২৮ byte, তাই প্রতিটি thread আলাদা খণ্ডে: ৩২টি transaction, ৪,০৯৬ byte এনে ১২৮ কাজে — মাত্র ৩.১%. Row-major matrix-এর একটি কলাম ধরে (৩২টি float-এর সারি থাকলে) thread চালালে ঠিক এটিই হয়।
  5. Stride 1 কিন্তু misaligned. Base 4 হলে ঠিকানা 4 থেকে 128 — শেষ thread-টি পরের খণ্ডে পড়ে। দুটি transaction, ৫০%. তাই GPU-র memory allocator সাধারণত array-কে বড় সীমানায় align করে দেয়।
  6. তুলনা। একই ১২৮ byte দরকারি ডেটার জন্য transaction ১ → ২ → ৪ → ৩২. Global memory-র bandwidth GPU-র সবচেয়ে মূল্যবান সম্পদ, তাই kernel লেখার প্রথম নিয়ম: পাশাপাশি thread যেন পাশাপাশি ঠিকানা পড়ে।

নিচের calculator যেকোনো stride, উপাদানের মাপ আর base ঠিকানার জন্য খণ্ড গোনে — আর একই access ৩২-byte “sector”-এ গুনে দেখায় (নতুন অনেক GPU আরও সূক্ষ্ম ৩২-byte একক ব্যবহার করে বলে প্রকাশিত):

// Memory coalescing: which 128-byte segments does one warp touch?
// Assumption (textbook model): 32 threads, thread t reads base + t*stride*size
const SEG = 128, WARP = 32;
function warpLoad(stride, size, base) {
  const segs = new Set();
  for (let t = 0; t < WARP; t++) {
    const first = base + t * stride * size, last = first + size - 1;
    for (let s = Math.floor(first / SEG); s <= Math.floor(last / SEG); s++) segs.add(s);
  }
  const useful = WARP * size, moved = segs.size * SEG;
  return { n: segs.size, moved, eff: (100 * useful / moved).toFixed(1) + "%" };
}
const cases = [ // [stride in elements, element bytes, base address]
  [1, 4, 0], [2, 4, 0], [4, 4, 0], [32, 4, 0], [1, 4, 4], [1, 8, 0], [1, 1, 0]];
console.log("stride size base | segments  bytes moved  efficiency");
for (const [st, sz, b] of cases) {
  const r = warpLoad(st, sz, b);
  console.log(String(st).padStart(6) + String(sz).padStart(5) + String(b).padStart(5) + " |" +
    String(r.n).padStart(9) + String(r.moved).padStart(13) + r.eff.padStart(12));
}
// Same accesses counted in 32-byte sectors (finer granularity, newer GPUs)
const sectors = (stride, size, base) => {
  const s = new Set();
  for (let t = 0; t < WARP; t++) s.add(Math.floor((base + t * stride * size) / 32));
  return s.size;
};
console.log("32-byte sectors, 4-byte elements: stride 1 -> " + sectors(1, 4, 0) +
  ", stride 2 -> " + sectors(2, 4, 0) + ", stride 32 -> " + sectors(32, 4, 0));

আউটপুট (নির্ধারিত):

stride size base | segments  bytes moved  efficiency
     1    4    0 |        1          128      100.0%
     2    4    0 |        2          256       50.0%
     4    4    0 |        4          512       25.0%
    32    4    0 |       32         4096        3.1%
     1    4    4 |        2          256       50.0%
     1    8    0 |        2          256      100.0%
     1    1    0 |        1          128       25.0%
32-byte sectors, 4-byte elements: stride 1 -> 4, stride 2 -> 8, stride 32 -> 32

প্রথম পাঁচ সারি stepper-এর সাথে হুবহু মেলে: stride 1, 2, 4, 32-এ খণ্ড 1, 2, 4, 32, আর base 4-এ stride 1 হলেও 2. ষষ্ঠ সারি: 8-byte double, stride 1 — ৩২ × ৮ = ২৫৬ byte, দুটি খণ্ড, কিন্তু দক্ষতা ১০০%, কারণ সবটাই দরকারি। শেষ সারি উল্টো শিক্ষা দেয়: ১-byte উপাদানে একটি warp মাত্র ৩২ byte চায়, অথচ একটি পুরো ১২৮-byte খণ্ড আসে — ২৫%; তাই ছোট টাইপে প্রতিটি thread-কে কয়েকটি উপাদান (যেমন ৪টি byte একসাথে) দেওয়া হয়। Sector-এর হিসাবে stride 1, 2, 32-এ 4, 8, 32টি sector — সূক্ষ্ম একক অপচয় কিছুটা কমায়, কিন্তু stride 32-এর সর্বনাশ থেকে বাঁচায় না।

Shared memory আর bank conflict

Shared memory একসাথে অনেক thread-কে ডেটা দিতে পারে কারণ সেটি কয়েকটি স্বাধীন bank-এ ভাগ — অনেক NVIDIA GPU-তে ৩২টি bank, প্রতিটি 4 byte চওড়া; পরপর 4-byte word পরপর bank-এ: bank = (ঠিকানা / 4) mod 32. একটি warp-এর ৩২টি thread ৩২টি আলাদা bank ছুঁলে এক cycle-এ সব। দুটি thread একই bank-এর আলাদা word চাইলে সেগুলো পরপর পরিবেশন হয় — একে বলে bank conflict. (একই word হলে সমস্যা নেই — broadcast হয়।) Word-এর stride 1-এ conflict নেই; stride 2-এ ২-way (দুই গুণ সময়); stride 32-এ সব thread একই bank-এ — ৩২-way. প্রচলিত সমাধান padding: ৩২ × ৩২-এর একটি tile [32][33] হিসেবে ঘোষণা করলে কলাম ধরে পড়ার stride হয় ৩৩, আর ৩৩ mod ৩২ = ১ — conflict উধাও।

Tiled matrix multiply — সব মিলিয়ে

n × n matrix গুণে C-এর প্রতিটি উপাদানের জন্য A-এর একটি সারি আর B-এর একটি কলাম লাগে — ২n বার global memory থেকে পড়া। কিন্তু একই সারি আর কলাম বহু thread-এর দরকার। পর্ব ৪৮-এর blocking-এর ধারণাই এখানে: একটি block A আর B-এর একটি করে ১৬ × ১৬ tile একবার shared memory-তে আনে, তারপর সব thread সেখান থেকে ১৬ বার করে পড়ে:

#define T 16                                   // tile = 16 x 16 = one block
__global__ void matmul(const float *A, const float *B, float *C, int n) {
    __shared__ float As[T][T], Bs[T][T];       // per-block scratchpad tiles
    int tx = threadIdx.x, ty = threadIdx.y;
    int row = blockIdx.y * T + ty;
    int col = blockIdx.x * T + tx;
    float sum = 0.0f;                          // lives in a register
    for (int k0 = 0; k0 < n; k0 += T) {        // walk tiles along k
        As[ty][tx] = A[row * n + (k0 + tx)];   // neighbours read neighbours
        Bs[ty][tx] = B[(k0 + ty) * n + col];   //   -> coalesced loads
        __syncthreads();                       // whole tile loaded?
        for (int k = 0; k < T; k++)
            sum += As[ty][k] * Bs[k][tx];      // 16 MACs from shared memory
        __syncthreads();                       // done before tile is reused
    }
    C[row * n + col] = sum;
}
// launch: dim3 block(T, T); dim3 grid(n / T, n / T);
//         matmul<<<grid, block>>>(dA, dB, dC, n);

লাইন ধরে: __shared__ দুটি tile ঘোষণা করে, যা block-এর সব thread দেখে। প্রতিটি thread C-এর একটি উপাদান (row, col) হিসাব করে, আংশিক যোগফল sum থাকে register-এ। বাইরের লুপ k-অক্ষ বরাবর tile ধরে এগোয়। Load-এর দুটি লাইনে পাশাপাশি threadIdx.x পাশাপাশি ঠিকানা পড়ে — coalesced. প্রথম __syncthreads() নিশ্চিত করে tile পুরো ভরার আগে কেউ হিসাব শুরু করবে না; ভেতরের লুপে ১৬টি গুণ-যোগ শুধু shared memory থেকে (As[ty][k] একই সারির ১৬টি thread-এর কাছে একই word — broadcast; Bs[k][tx] পাশাপাশি bank — conflict নেই)। দ্বিতীয় barrier নিশ্চিত করে কেউ পুরোনো tile ব্যবহার শেষ না করা পর্যন্ত নতুন tile লেখা হবে না। ফল: global memory থেকে পড়া T = ১৬ গুণ কম — n = ১০২৪-এ সরল রূপে ২n³ = ২,১৪,৭৪,৮৩,৬৪৮টি load, tiled রূপে ১৩,৪২,১৭,৭২৮টি।

৬ · CPU থেকে GPU-তে ডেটা পাঠানো — PCIe-র খরচ আর কখন offload লাভজনক

আলাদা graphics card-এর GPU-র নিজের মেমরি, CPU-র নিজের। ডেটা যায় PCIe bus দিয়ে (পর্ব ৫১), যার bandwidth GPU-র নিজের মেমরির তুলনায় অনেক কম — প্রায়ই দশ গুণ বা তারও বেশি কম। আর প্রতিটি kernel চালু করতেও কয়েক মাইক্রোসেকেন্ডের মতো একটি স্থির খরচ (launch overhead) আছে। তাই প্রশ্নটি সবসময়: হিসাবের লাভ কি পাঠানোর খরচ পুষিয়ে দেবে? নিচের সংখ্যাগুলো শুধু হিসাবের জন্য ধরে নেওয়া, কোনো নির্দিষ্ট পণ্যের নয়: PCIe কার্যকর ২৫ GB/s; CPU টানা ১০০ GFLOP/s আর মেমরি ৫০ GB/s; GPU টানা ১০ TFLOP/s আর মেমরি ১ TB/s.

হিসাব ১ — vector add, n = ১০৮টি float

পড়া-লেখা: দুটি input + একটি output = ১২ × ১০৮ byte = ১.২ GB; হিসাব মাত্র ১০৮টি যোগ। CPU: মেমরি-সীমিত, ১.২ GB / ৫০ GB/s = ২৪ ms. GPU: PCIe-তে ১.২ GB / ২৫ GB/s = ৪৮ ms, তারপর নিজের মেমরিতে ১.২ GB / ১ TB/s = ১.২ ms — মোট ৪৯.২ ms. GPU হারে প্রায় দুই গুণ ব্যবধানে, কারণ প্রতি byte-এ হিসাব প্রায় নেই। ডেটা আগে থেকেই GPU-তে থাকলে (পরপর অনেক kernel) তখন ১.২ ms — পুরো ছবি উল্টে যায়।

হিসাব ২ — n × n float matrix গুণ, break-even

হিসাব ২n³ FLOP, পাঠানো ৩টি matrix = ১২n² byte. CPU সময় = ২n³ / ১০১১; GPU সময় = ১২n² / (২.৫ × ১০১০) + ২n³ / ১০১৩. সমান ধরে n দিয়ে ভাগ: ২n (১০−১১ − ১০−১৩) = ১২ / (২.৫ × ১০১০) → n ≈ ২৪.২. অর্থাৎ launch খরচ বাদ দিলে n ≥ ২৫-এ GPU এগিয়ে। প্রতি kernel ১০ µs launch খরচ যোগ করলে break-even সরে যায় প্রায় n = ৮৯-এ (CPU ১৪.১ µs বনাম GPU ১৩.৯ µs). n = ১০০০-এ CPU ২০ ms, GPU ০.৪৮ ms পাঠানো + ০.২ ms হিসাব = ০.৬৮ ms — প্রায় ২৯ গুণ. কারণ: হিসাব বাড়ে n³-এ, পাঠানো n²-এ।

এই অনুপাত — প্রতি byte আনা-নেওয়ায় কতটি FLOP — এর নাম arithmetic intensity. Vector add-এ ১ / ১২ FLOP/byte, n = ১০০০-এর matmul-এ ২ × ১০৯ / (১.২ × ১০৭) ≈ ১৬৭. পর্ব ৫৪-এর roofline model এই অনুপাত দিয়েই বলে একটি kernel মেমরি-সীমিত না হিসাব-সীমিত। Laptop আর ফোনের chip-এ, যেখানে CPU আর GPU একই মেমরি ভাগ করে (integrated GPU, অনেক SoC), PCIe-র এই খরচ নেই — তবে মেমরির মোট bandwidth-ও তুলনায় কম।

৭ · AI accelerator — tensor core, systolic array আর কম precision

একটি neural network-এর প্রতিটি স্তর আসলে একটি বড় matrix গুণ: input-এর সারি × weight-এর matrix, তারপর একটি সরল function. Model train করা আর তাকে ব্যবহার করে উত্তর বানানো (inference) — দুটিতেই সময়ের সিংহভাগ যায় ঘন matrix গুণ আর যোগে, অর্থাৎ কোটি কোটি MAC (multiply-accumulate). Matrix গুণের বিশেষ গুণ: n × n-এ ডেটা n², হিসাব n³ — প্রতিটি সংখ্যা n বার ব্যবহার হয়। যে hardware এই পুনর্ব্যবহার chip-এর ভেতরে ধরে রাখতে পারে, সে প্রতি byte-এ অনেক বেশি হিসাব করতে পারে।

Tensor core আর matrix unit

সাধারণ GPU lane প্রতি instruction-এ একটি FMA করে। Tensor core (NVIDIA-র Volta, ২০১৭ থেকে) একটি instruction-এ একটি ছোট matrix-খণ্ডের পুরো গুণ-যোগ করে — যেমন প্রথম প্রজন্মে ৪ × ৪ FP16 matrix-এর গুণ, FP32-এ জমা, প্রকাশিত বিবরণ অনুযায়ী। Warp-এর ৩২টি thread মিলে বড় একটি খণ্ড (যেমন ১৬ × ১৬) নেয়। Instruction fetch-decode আর register-এর পড়া-লেখার খরচ এক instruction-এ বহু MAC-এ ভাগ হয়ে যায় — পর্ব ৪২-এর SIMD-যুক্তির আরেক ধাপ। AMD-এর matrix core, Intel-এর AMX, ARM-এর SME — বিভিন্ন নির্মাতা একই ধারণার নিজস্ব রূপ এনেছে।

Systolic array — ডেটা হাঁটে, হিসাব হয় পথে

১৯৭৮ সালের দিকে H. T. Kung আর Charles Leiserson systolic array-এর ধারণা দেন, আর ১৯৮২ সালে Kung-এর “Why Systolic Architectures?” প্রবন্ধ একে পরিচিত করে। নামটি এসেছে হৃৎপিণ্ডের systole থেকে — প্রতিটি “স্পন্দনে” (clock) ডেটা এক ঘর এগোয়। একটি grid-এ সাজানো অনেকগুলো সরল processing element (PE), প্রতিটি শুধু গুণ-যোগ করে আর ডেটা প্রতিবেশীকে দিয়ে দেয়। একটি সংখ্যা মেমরি থেকে একবার ঢোকে, তারপর পথে বহু PE তাকে ব্যবহার করে — মেমরিতে ফেরত না গিয়ে।

একটি বিয়েবাড়িতে লম্বা টেবিলে মেহমানরা বসে আছেন। খাবারের বাটি বাঁ দিক থেকে হাতে হাতে ডানে যাচ্ছে, আর ওপরের সারি থেকে প্লেট নিচে নামছে। প্রত্যেকে নিজের কাছে আসা বাটি থেকে নিজের প্লেটে নেন, তারপর বাটি পাশের জনকে দেন। কাউকে রান্নাঘরে (মেমরি) উঠে যেতে হয় না — সব কাজ হয় হাত-বদলের পথে।

এখানে একটি output-stationary নকশা: n × n PE, PE(i, j) জমায় C[i][j]. A-এর সারি i বাঁ দিক থেকে ঢোকে, i cycle দেরিতে; B-এর কলাম j ওপর থেকে নামে, j cycle দেরিতে — এই “তির্যক” (skewed) খাওয়ানোর ফলে PE(i, j)-তে a[i][k] আর b[k][j] একই cycle-এ পৌঁছায়, cycle t = i + j + k (0 থেকে গোনা)। শেষ MAC হয় t = ৩(n − ১)-এ, তাই মোট ৩n − ২ cycle. A = [[1,2,3],[4,5,6],[7,8,9]], B = [[9,8,7],[6,5,4],[3,2,1]]:

PE(i, j) জমায় C[i][j]; A ডানে হাঁটে, B নিচে নামে; “—” মানে শূন্য বা কিছু ঢোকেনি
Cycle
বাঁ থেকে ঢুকছে (A, সারি 0/1/2)
ওপর থেকে ঢুকছে (B, কলাম 0/1/2)
PE সারি 0
PE সারি 1
PE সারি 2
ব্যস্ত PE
  1. Cycle 1 — শুধু কোণের PE. a[0][0] = 1 আর b[0][0] = 9 PE(0,0)-তে মেলে: 1 × 9 = 9. বাকি সারি-কলাম এখনো দেরিতে, তাই আটটি PE অলস।
  2. Cycle 2 — ঢেউ ছড়ায়। PE(0,0): 9 + 2 × 6 = 21. আগের 1 ডানে সরে PE(0,1)-এ b[0][1] = 8-এর সাথে: 8. আগের 9 নিচে নেমে PE(1,0)-এ a[1][0] = 4-এর সাথে: 36. তিনটি PE ব্যস্ত — i + j + k = 1-এর তিনটি জোড়।
  3. Cycle 3. PE(0,0) শেষ: 21 + 3 × 3 = 30 = C[0][0]. তির্যক রেখা i + j ≤ 2-এর ছয়টি PE ব্যস্ত; PE(2,0) পায় 7 × 9 = 63, PE(0,2) পায় 1 × 7 = 7.
  4. Cycle 4 — সর্বোচ্চ ভিড়। সাতটি PE একসাথে: যেমন PE(1,1) = 32 + 5 × 5 = 57, PE(2,0) = 63 + 8 × 6 = 111. দুটি উপাদান শেষ হলো: C[0][1] = 24 আর C[1][0] = 84.
  5. Cycle 5. ছয়টি PE ব্যস্ত; এবার শেষ হলো C[0][2] = 18, C[1][1] = 69 আর C[2][0] = 111 + 9 × 3 = 138.
  6. Cycle 6. ঢেউ নিচে-ডানে সরে যাচ্ছে: তিনটি PE ব্যস্ত, C[1][2] = 54, C[2][1] = 114.
  7. Cycle 7 — শেষ MAC. PE(2,2): 81 + 9 × 1 = 90. মোট ৩n − ২ = ৭ cycle-এ ২৭টি MAC. গড় ব্যবহার ২৭ / (৭ × ৯) ≈ ৪২.৯% — ভরা আর খালি হওয়ার সময়ের জন্য।
  8. যাচাই। সাধারণ গুণে C = [[30,24,18],[84,69,54],[138,114,90]] — হুবহু মিলেছে। বাস্তবে অনেকগুলো matrix পরপর ঢোকালে একটির খালি হওয়ার সময়ে পরেরটি ভরে, ব্যবহার ১০০%-এর কাছে যায়। ফল PE থেকে বের করার জন্যও কিছু cycle লাগে, যা এখানে গোনা হয়নি।

নিচের simulator আসল ডেটা-চলাচল মডেল করে — প্রতি cycle-এ a-register ডানে, b-register নিচে এক ঘর সরে, তারপর প্রতিটি PE গুণ-যোগ করে — আর শেষে সাধারণ matmul-এর সাথে মেলায়:

// Output-stationary systolic array: n x n PEs, A flows right, B flows down
const A = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
const B = [[9, 8, 7], [6, 5, 4], [3, 2, 1]];
const n = A.length, Z = () => A.map(r => r.map(() => 0));
let a = Z(), b = Z(), acc = Z(), cycles = 0, macs = 0;
for (let t = 0; t <= 3 * n - 3; t++) {          // last MAC happens at t = 3n - 3
  for (let i = 0; i < n; i++) for (let j = n - 1; j > 0; j--) a[i][j] = a[i][j - 1];
  for (let j = 0; j < n; j++) for (let i = n - 1; i > 0; i--) b[i][j] = b[i - 1][j];
  for (let i = 0; i < n; i++) {                   // skewed feed: row i starts i cycles late
    const k = t - i; a[i][0] = k >= 0 && k < n ? A[i][k] : 0;
  }
  for (let j = 0; j < n; j++) {
    const k = t - j; b[0][j] = k >= 0 && k < n ? B[k][j] : 0;
  }
  let busy = 0;
  for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) {
    const k = t - i - j;                          // PE(i,j) sees a[i][k] and b[k][j] now
    if (k >= 0 && k < n) { acc[i][j] += a[i][j] * b[i][j]; busy++; }
  }
  cycles++; macs += busy;
  console.log("cycle " + cycles + " (" + busy + " PEs busy): " +
    acc.map(r => "[" + r.map(x => String(x).padStart(3)).join(" ") + "]").join(" "));
}
const ref = A.map((r, i) => B[0].map((_, j) => r.reduce((s, x, k) => s + x * B[k][j], 0)));
const ok = ref.every((r, i) => r.every((x, j) => x === acc[i][j]));
console.log("total cycles = " + cycles + " (3n - 2), MACs = " + macs + " (n^3)");
console.log("matches normal matmul: " + ok + ", utilisation = " +
  (100 * macs / (cycles * n * n)).toFixed(1) + "%");

আউটপুট (নির্ধারিত):

cycle 1 (1 PEs busy): [  9   0   0] [  0   0   0] [  0   0   0]
cycle 2 (3 PEs busy): [ 21   8   0] [ 36   0   0] [  0   0   0]
cycle 3 (6 PEs busy): [ 30  18   7] [ 66  32   0] [ 63   0   0]
cycle 4 (7 PEs busy): [ 30  24  15] [ 84  57  28] [111  56   0]
cycle 5 (6 PEs busy): [ 30  24  18] [ 84  69  48] [138  96  49]
cycle 6 (3 PEs busy): [ 30  24  18] [ 84  69  54] [138 114  81]
cycle 7 (1 PEs busy): [ 30  24  18] [ 84  69  54] [138 114  90]
total cycles = 7 (3n - 2), MACs = 27 (n^3)
matches normal matmul: true, utilisation = 42.9%

প্রতিটি লাইন stepper-এর একটি cycle — তিনটি বন্ধনী তিনটি PE-সারি। ব্যস্ত PE 1, 3, 6, 7, 6, 3, 1 — যোগফল ২৭ = n³, cycle ৭ = ৩n − ২, আর ফল সাধারণ matmul-এর সাথে true. A, B 4 × 4 করে চালালে ১০ cycle আর ৬৪টি MAC পাবেন (ব্যবহার ৪০%). Google-এর প্রথম TPU-তে (২০১৫-১৬-এর দিকে ব্যবহার শুরু, ২০১৭ সালে প্রবন্ধ প্রকাশ) ২৫৬ × ২৫৬ = ৬৫,৫৩৬টি 8-bit MAC-এর একটি systolic array ছিল বলে প্রকাশিত; তার নকশা ছিল weight-stationary — weight PE-তে বসে থাকে, input হাঁটে — এখানকার output-stationary থেকে আলাদা, কিন্তু মূল ধারণা একই।

কম precision — কম bit, বেশি গুণ

Neural network ছোট ছোট গোলমাল সহ্য করতে পারে, তাই 32-bit float সবসময় লাগে না। পর্ব ২৩-এ যে format-গুলো দেখেছেন: FP16 (৫ bit exponent, ১০ bit fraction), BF16 (FP32-এর সমান ৮ bit exponent, মাত্র ৭ bit fraction — পরিসর বড়, নির্ভুলতা কম), FP8 (E4M3 আর E5M2 রূপ), আর integer INT8, এমনকি ৪-bit. একটি 8-bit গুণক একটি 32-bit গুণকের চেয়ে অনেক ছোট — গুণকের জায়গা মোটামুটি bit-সংখ্যার বর্গের মতো বাড়ে (পর্ব ২১) — তাই একই silicon-এ অনেক বেশি MAC, আর প্রতিটি সংখ্যা আনতে কম byte. সাধারণ কৌশল mixed precision: গুণ কম precision-এ, যোগফল জমানো FP32-এ। Float model-কে INT8-এর মতো integer-এ রূপান্তর করাকে বলে quantization — নির্ভুলতা কতটা কমে, তা model আর কৌশলভেদে আলাদা, মেপে দেখতে হয়।

TOPS বনাম আসল কাজ

AI chip-এর বিজ্ঞাপনে প্রায়ই TOPS (প্রতি সেকেন্ডে ট্রিলিয়ন operation) লেখা থাকে। হিসাবটি সরল: MAC-এর সংখ্যা × ২ (গুণ আর যোগ আলাদা গোনা) × clock. একটি কাল্পনিক NPU: ৪,০৯৬টি INT8 MAC, ১ GHz → ৪,০৯৬ × ২ × ১০৯ = ৮.১৯২ TOPS চূড়ান্ত (peak). এটি তখনই সম্ভব যখন প্রতিটি MAC প্রতিটি cycle-এ ব্যস্ত — বাস্তবে মেমরি থেকে ডেটা আসতে দেরি, ছোট বা অদ্ভুত আকারের স্তর, আর matrix ছাড়া অন্য কাজের জন্য ব্যবহার অনেক কম হতে পারে; ৩০% হলে ২.৪৬ TOPS. তাছাড়া কোন precision-এ (INT8 না INT4), আর sparsity (শূন্য বাদ দেওয়া) ধরে সংখ্যা দ্বিগুণ দেখানো হয়েছে কি না — এসব না জেনে দুটি chip-এর TOPS তুলনা অর্থহীন।

ফোনে আর সাম্প্রতিক laptop-এর SoC-তে আলাদা NPU (neural processing unit) থাকে — ছবি থেকে পেছনের দৃশ্য ঝাপসা করা, কথা থেকে লেখা, ছোট ভাষা-model — কম power-এ চালানোর জন্য। এগুলোর ভেতরের নকশা নির্মাতাভেদে আলাদা আর প্রায়ই পুরো প্রকাশিত নয়; সাধারণত ছোট systolic বা MAC-array, বড় on-chip SRAM আর INT8/FP16-এর মতো কম precision থাকে বলে জানা যায়।

AI inference-এর আসল ছাদ — মেমরির bandwidth

একটি বড় ভাষা-model একটি একটি করে token (শব্দাংশ) বানায়। একজন ব্যবহারকারীর জন্য (batch = ১) প্রতিটি token-এ model-এর প্রায় সব weight একবার মেমরি থেকে পড়তে হয়, আর প্রতিটি weight-এ মাত্র একটি গুণ-যোগ। তাই সময়ের নিচের সীমা হিসাব দিয়ে নয়, bytes দিয়ে:

সূত্র আর হিসাব (ধরে নেওয়া সংখ্যা)

প্রতি token-এর সময় ≥ (parameter × bytes প্রতি parameter) / মেমরির bandwidth; token/s ≤ তার উল্টো।
একটি কাল্পনিক ৭০০ কোটি (৭ × ১০৯) parameter-এর model, ১ TB/s মেমরি:
FP16 (২ byte): ১৪ GB / ১ TB/s = ১৪ ms → সর্বোচ্চ ≈ ৭১.৪ token/s.
INT8 (১ byte): ৭ GB → ৭ ms → ≈ ১৪২.৯ token/s. ৪-bit (০.৫ byte): ৩.৫ GB → ≈ ২৮৫.৭ token/s.
একই model ১০০ GB/s-এর একটি laptop-এ FP16-এ ≈ ৭.১৪, ৪-bit-এ ≈ ২৮.৬ token/s.
হিসাবের চাহিদা: প্রতি token-এ ≈ ২ × ৭ × ১০৯ = ১৪ GFLOP; ৭১.৪ token/s-এ মাত্র ≈ ১ TFLOP/s — যেকোনো আধুনিক GPU-র সামর্থ্যের ছোট অংশ। অর্থাৎ কাজটি মেমরি-সীমিত।

এজন্যই AI accelerator-এ HBM-এর মতো দামি, চওড়া মেমরি (পর্ব ৪৯), আর quantization এত জনপ্রিয় — bit কমালে সরাসরি token/s বাড়ে। অনেক ব্যবহারকারীর অনুরোধ একসাথে (batch) চালালে একবার পড়া weight অনেক token-এ কাজে লাগে, arithmetic intensity বাড়ে — server-এ throughput বাড়ানোর মূল কৌশল এটাই। (বাস্তবে attention-এর জন্য জমানো তথ্য, KV cache, আরও bytes যোগ করে; এখানে হিসাব শুধু weight-এর।)

৮ · GPU, FPGA না ASIC — নমনীয়তা, energy আর খরচ

দিকGPUFPGAASIC (যেমন TPU, NPU)
নমনীয়তাসফটওয়্যারে যেকোনো data-parallel কাজHardware নিজেই নতুন করে সাজানো যায় (পর্ব ১৪)নির্দিষ্ট কাজের জন্য স্থির; বদলাতে নতুন chip
প্রতি watt-এ কাজভালোনির্দিষ্ট কাজে ভালো, সাধারণত clock কমনির্দিষ্ট কাজে সবচেয়ে ভালো
শুরুর খরচকম — কিনে চালানোমাঝারি — নকশা আর যাচাইয়ের প্রকৌশলঅনেক বেশি — নকশা, যাচাই, mask; বড় পরিমাণে বানালে তবেই লাভ
সফটওয়্যারপরিপক্ব (CUDA, ROCm, বিভিন্ন framework)কঠিন — HDL বা high-level synthesisনির্মাতার নিজস্ব compiler আর runtime
উপযুক্তগবেষণা, দ্রুত বদলানো model, সাধারণ HPCকম latency-র stream, network, prototypeবিশাল পরিমাণে একই কাজ — data center, ফোনের NPU

Energy-র দিকটি সবচেয়ে গভীর। আধুনিক chip-এ একটি গুণ-যোগের চেয়ে ডেটা সরানো অনেক দামি — বাইরের DRAM থেকে একটি সংখ্যা আনতে খরচ হয় একটি গুণের চেয়ে শত গুণ বা তারও বেশি energy, বহুল উদ্ধৃত হিসাব অনুযায়ী। তাই systolic array, shared memory-র tile আর কম precision — তিনটিই মূলত ডেটা-চলাচল কমানোর কৌশল। আর পর্ব ০২-এর power wall আর dark silicon মনে রাখলে বোঝা যায় কেন বিশেষায়িত accelerator বাড়ছে: সব transistor একসাথে চালানো যায় না, তাই নির্দিষ্ট কাজে দক্ষ unit বসিয়ে দরকারের সময় চালানোই লাভজনক। দামের দিক থেকে বড় AI accelerator আর তার HBM মেমরি খুব ব্যয়বহুল, data center-এর বিদ্যুৎ আর ঠান্ডা রাখার খরচও বড় — তাই performance per watt আর প্রতি টাকায় কাজ আজ গতির মতোই গুরুত্বপূর্ণ মাপকাঠি। পরের পর্বে power আর energy-র হিসাব বিস্তারিত।

আরও গভীরে: একটি SM-এর ভেতরে সাধারণত কয়েকটি warp scheduler থাকে, প্রতিটি প্রতি cycle-এ নিজের warp-গুলো থেকে একটি প্রস্তুত warp বাছে; register file এত বড় যে প্রায়ই তা SM-এর L1-এর চেয়েও বড়, আর register পড়ার bandwidth সামলাতে তাকে অনেক bank-এ ভাগ করা হয়। বড় AI GPU-তে আজ কয়েকটি chiplet আর HBM stack একই package-এ বসানো হয়, আর একাধিক GPU-কে PCIe-র চেয়ে দ্রুত বিশেষ link-এ জোড়া হয় — বড় model train করতে হাজার GPU-র মধ্যে ডেটা আদান-প্রদানই প্রায়ই মূল বাধা। গবেষণায় processing-in-memory (মেমরি chip-এর ভেতরেই হিসাব) আর analog বা optical MAC-ও পরীক্ষা চলছে।

পরীক্ষা ও ইন্টারভিউতে — কয়েকটি প্রচলিত ধরনের প্রশ্ন আর এক লাইনের উত্তর:

  • SIMT আর SIMD-এর পার্থক্য কী? SIMD-এ প্রোগ্রামার/compiler lane আর mask সামলায়; SIMT-এ প্রতিটি lane আলাদা thread হিসেবে লেখা, divergence-এর mask hardware সামলায়।
  • Branch divergence কী, দক্ষতা কীভাবে হিসাব করবেন? Warp-এর thread-রা আলাদা পথে গেলে পথগুলো mask দিয়ে পালা করে চলে; দক্ষতা = সক্রিয় lane-slot / (issue × warp-এর আকার)।
  • Memory coalescing কেন জরুরি? পাশাপাশি thread পাশাপাশি ঠিকানা পড়লে একটি warp-এর load এক-দুটি transaction-এ হয়; stride বাড়লে ৩২টি পর্যন্ত, bandwidth অপচয়।
  • Occupancy কী আর কী দিয়ে সীমিত? জীবিত warp / সর্বোচ্চ warp; register, shared memory আর thread/block-এর সীমার সবচেয়ে ছোটটি ঠিক করে।
  • n × n systolic array-তে একটি n × n গুণে কত cycle? Output-stationary, skewed input-এ ৩n − ২ cycle (ফল বের করার সময় বাদে)।

৯ · AI-এর যুগে — GPU-র প্রশ্নে AI, আর যাচাইয়ের অভ্যাস

মজার ব্যাপার, যে GPU-তে AI চলে, তার সম্পর্কে AI-কে জিজ্ঞেস করলে প্রায়ই আত্মবিশ্বাসী ভুল আসে। এই বিষয়ে সাধারণ ভুলগুলো:

  • নির্দিষ্ট GPU-র SM-সংখ্যা, register, shared memory, TOPS বা bandwidth বানিয়ে বলা, বা এক প্রজন্মের সংখ্যা আরেক প্রজন্মে বসানো।
  • Divergence-এর সময়কে দুই পথের যোগফলের বদলে সর্বোচ্চটি ধরা; occupancy-তে shared memory-র সীমা ভুলে যাওয়া; coalescing-এ misalignment বা উপাদানের মাপ উপেক্ষা।
  • CUDA kernel-এ if (i < n) পাহারা বা __syncthreads() বাদ দেওয়া, বা শাখার ভেতরে barrier বসানো (সব thread না পৌঁছালে আটকে যেতে পারে)।
  • PCIe-তে ডেটা পাঠানোর খরচ বাদ দিয়ে “GPU-তে ১০০ গুণ দ্রুত” দাবি; AMD-র wavefront আর NVIDIA-র warp-এর আকার গুলিয়ে ফেলা।
  • TOPS-কে আসল গতি ধরা, precision বা sparsity উল্লেখ না করা; token/s-এর হিসাবে মেমরির bandwidth না দেখা।

AI-কে ব্যাখ্যাকারী আর পরীক্ষক হিসেবে ব্যবহার করুন — প্রতিটি সংখ্যার উৎস আর হিসাবের ধাপ চেয়ে নিন:

আমি GPU আর্কিটেকচার শিখছি। নিচের CUDA kernel-টি বিশ্লেষণ করো, কিন্তু: ১. প্রতিটি global memory access-এ একটি warp কয়টি ১২৮-byte খণ্ড ছোঁয় — ঠিকানার সূত্র লিখে দেখাও। ২. কোনো branch-এ divergence হবে কি না, হলে SIMD দক্ষতা কত — ধাপে ধাপে হিসাব করো। ৩. প্রতি thread-এর register আর প্রতি block-এর shared memory থেকে occupancy হিসাব করো; কোন GPU-র সীমা ধরছ, তা স্পষ্ট লেখো, না জানলে বলো “জানি না”। ৪. কোনো নির্দিষ্ট GPU-র সংখ্যা বললে উৎস বলো, অনুমান হলে অনুমান বলো। ৫. ফল যাচাইয়ের জন্য একটি CPU-তে চলা JavaScript বা C simulator দাও। Kernel: [এখানে আপনার kernel লিখুন]

AI-এর দেওয়া GPU-হিসাব যাচাইয়ের চেকলিস্ট

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

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

প্র ০১গেম খেলার জন্য বানানো একটি chip কীভাবে AI-যুগের সবচেয়ে গুরুত্বপূর্ণ hardware হয়ে উঠল? এর পেছনে প্রযুক্তি, সফটওয়্যার আর ভাগ্য — কোনটির ভূমিকা কতটা?

প্রযুক্তির দিক থেকে graphics আর neural network একই ধরনের কাজ: লাখো pixel বা লাখো neuron-এ একই হিসাব, একে অপরের থেকে স্বাধীন। একটি 3D দৃশ্যের প্রতিটি vertex-কে একটি ৪ × ৪ matrix দিয়ে গুণ করা আর একটি neural network-এর স্তরে input-কে weight matrix দিয়ে গুণ করা — গণিতে প্রায় এক। গেমের বাজার প্রতি বছর আরও বাস্তব ছবি চাইত, তাই GPU নির্মাতারা দুই দশক ধরে throughput আর মেমরির bandwidth বাড়িয়ে গেছেন — আর সেই খরচ বহন করেছে কোটি কোটি gaming card-এর বিক্রি। বিশেষায়িত বৈজ্ঞানিক hardware এই আকারের বাজার কখনো পেত না।

কিন্তু শুধু hardware যথেষ্ট ছিল না। ২০০০-এর দশকের শুরুর GPGPU কাজে হিসাবকে texture আর pixel-এর ছদ্মবেশে লিখতে হতো — কঠিন আর ভঙ্গুর। ২০০৬-০৭-এ unified shader নকশা আর CUDA সেই বাধা সরায়: C-এর মতো ভাষা, thread আর block-এর সরল মডেল, আর ধীরে ধীরে matrix গুণ, FFT, পরে deep learning-এর library. OpenCL খোলা মান হিসেবে এসেছিল, কিন্তু অনেক বছর ধরে library আর tool-এর পরিপক্বতায় CUDA-র ecosystem এগিয়ে থাকে — অনেক বিশ্লেষক মনে করেন এই সফটওয়্যার-ভিত্তিই পরে বাজারে বড় সুবিধা দিয়েছে।

তারপর এল সময়ের মিল। Neural network-এর ধারণা কয়েক দশক পুরোনো, কিন্তু বড় ডেটাসেট (যেমন লক্ষ লক্ষ লেবেল-দেওয়া ছবি) আর যথেষ্ট হিসাব-শক্তি একসাথে ছিল না। ২০১২ সালে AlexNet দুটি সাধারণ gaming GPU-তে train করে ছবি চেনার প্রতিযোগিতায় আগের পদ্ধতিগুলোকে বড় ব্যবধানে হারায় — গবেষকেরা বুঝলেন, GPU-তে আগে যা মাসে হতো তা দিনে সম্ভব। এরপর hardware নিজেই AI-এর দিকে বাঁক নেয়: ২০১৭-তে tensor core, FP16 আর পরে BF16, FP8, বিশাল HBM.

তাই উত্তরটি তিনটিরই মিশ্রণ: data-parallel নকশার সাধারণত্ব (প্রযুক্তি), প্রোগ্রাম করা সহজ করার দীর্ঘ বিনিয়োগ (সফটওয়্যার), আর সঠিক সময়ে বড় ডেটা ও নতুন অ্যালগরিদমের আগমন (ভাগ্য)। এর শিক্ষা হলো, একটি আর্কিটেকচারের সাফল্য শুধু transistor দিয়ে আসে না — বাজার যে খরচ জোগায় আর প্রোগ্রামার যে টুল পান, সেটাও নকশার অংশ।

মূল উপলব্ধি: বড় বাজারে টিকে থাকা সাধারণ-উদ্দেশ্যের throughput hardware আর তার সহজ প্রোগ্রামিং মডেল মিলে নতুন ধরনের কাজের জন্য প্রস্তুত হয়ে ছিল; AI এসে সেই প্রস্তুতিকে কাজে লাগিয়েছে।

প্র ০২GPU প্রতিটি thread-কে স্বাধীন দেখায়, অথচ ভেতরে warp একসাথে চলে। এই “বিভ্রম” কেন বেছে নেওয়া হলো, আর এর দাম কী?

SIMD-এর পুরোনো সমস্যা ছিল প্রোগ্রাম লেখা। পর্ব ৪২-এ দেখেছেন, SSE বা AVX-এ প্রোগ্রামারকে নিজে lane-এর চওড়া জানতে হয়, শর্তকে mask-এ বদলাতে হয়, tail সামলাতে হয়, আর register-এর চওড়া বদলালে কোড নতুন করে লিখতে হয়। SIMT এই ভার hardware-এ সরায়: প্রোগ্রামার একটি সাধারণ scalar thread লেখেন — নিজের if, নিজের লুপ — আর hardware সেগুলোকে warp-এ দলবদ্ধ করে mask আর reconvergence নিজে সামলায়। একই kernel বেশি বা কম SM-এর GPU-তে চলে, কারণ block-গুলো যেকোনো SM-এ যেকোনো ক্রমে বসতে পারে।

দাম আসে তখন, যখন বিভ্রমটি ভাঙে। প্রথমত, divergence: একটি warp-এর ৩২টি thread-এর মধ্যে একটিও আলাদা পথে গেলে পুরো warp দুই পথের সময় নেয়। গভীর, ডেটা-নির্ভর শাখা — যেমন একটি tree-তে খোঁজা বা ray tracing-এ ভিন্ন দিকে ছোটা রশ্মি — দক্ষতাকে ১/৩২-এ নামাতে পারে। দ্বিতীয়ত, মেমরি: প্রতিটি thread নিজের মতো ঠিকানা পড়লে coalescing ভাঙে, আর সঠিক কোড ৩২ গুণ ধীর হতে পারে — কোডের অর্থে কিছুই ভুল নেই, ভুল শুধু কর্মক্ষমতায়। তৃতীয়ত, সূক্ষ্ম সঠিকতার প্রশ্ন: পুরোনো GPU-তে একই warp-এর thread-রা একে অপরের জন্য spin-lock-এ অপেক্ষা করলে deadlock হতে পারত, কারণ divergent পথগুলো নির্দিষ্ট ক্রমে চলত। Volta (২০১৭) থেকে প্রতি thread-এর নিজের PC রাখা হয় বলে প্রকাশিত — এতে এমন কোড নিরাপদ হয়, কিন্তু “warp সবসময় একসাথে চলে” ধরে নেওয়া পুরোনো কোড ভাঙতে পারে, তাই নতুন CUDA-তে warp-স্তরের function-এ স্পষ্ট mask দিতে হয়।

অর্থাৎ SIMT হলো “সঠিকতার জন্য scalar, কর্মক্ষমতার জন্য vector”. নবীন প্রোগ্রামার দ্রুত সঠিক কোড লেখেন, কিন্তু দ্রুত কোড লিখতে warp, mask আর coalescing মাথায় রাখতেই হয় — ঠিক যেমন CPU-তে cache না বুঝে দ্রুত কোড লেখা যায় না (পর্ব ৪৮)।

মূল উপলব্ধি: ভালো abstraction সঠিকতা সহজ করে, কিন্তু কর্মক্ষমতা লুকিয়ে রাখা hardware-এর বাস্তবতা থেকেই আসে; SIMT-এ সেই বাস্তবতা হলো warp, divergence আর coalescing.

প্র ০৩Systolic array-এর ধারণা ১৯৭০-৮০-র দশকের। এত বছর পর কেন সেটি AI chip-এর হৃদয় হয়ে ফিরে এল?

Kung আর Leiserson-এর সময়ে VLSI (একটি chip-এ হাজার হাজার transistor) নতুন। তাঁদের যুক্তি ছিল, chip-এ তার লম্বা হলে ধীর আর দামি, তাই নকশা হওয়া উচিত নিয়মিত, পুনরাবৃত্ত আর শুধু প্রতিবেশীর সাথে যুক্ত সরল ঘর দিয়ে — আর মেমরি থেকে একবার আনা ডেটা পথে বহুবার ব্যবহার। Matrix গুণ, convolution, signal processing-এর জন্য বহু systolic নকশা প্রস্তাবিত হয়, কিছু বিশেষ যন্ত্রে বানানোও হয়। কিন্তু সাধারণ বাজারে সেগুলো টেকেনি: প্রতিটি নির্দিষ্ট অ্যালগরিদমের জন্য আলাদা chip লাভজনক ছিল না, আর তখন প্রতি দুই বছরে সাধারণ microprocessor দ্বিগুণ দ্রুত হচ্ছিল (Dennard scaling, পর্ব ০২) — বিশেষায়িত chip বানাতে বানাতে সাধারণ CPU-ই তাকে ধরে ফেলত।

২০১০-এর দশকে তিনটি জিনিস বদলায়। এক, Dennard scaling থেমে যায়, clock আর বাড়ে না; সাধারণ CPU আর প্রতি বছর আপনাআপনি দ্রুত হয় না, তাই বিশেষায়িত hardware-এর সুবিধা টিকে থাকে। দুই, একটি নির্দিষ্ট কাজ — neural network-এর matrix গুণ — হঠাৎ এত বিশাল পরিমাণে চলতে শুরু করে যে শুধু তার জন্য chip বানানো অর্থনৈতিকভাবে যুক্তিসঙ্গত। প্রকাশিত বিবরণ অনুযায়ী Google হিসাব করেছিল, কণ্ঠস্বর চেনার মতো সেবা ব্যাপকভাবে ব্যবহার হলে প্রচলিত hardware-এ data center-এর সংখ্যা অনেক বাড়াতে হতো — সেখান থেকে TPU. তিন, energy: ডেটা সরানো গুণের চেয়ে অনেক দামি, আর systolic array ঠিক এই খরচটিই কমায় — একটি weight PE-তে বসে থাকে, input হেঁটে যায়, আংশিক যোগফল পাশের ঘরে, DRAM-এ যাওয়ার দরকার পড়ে না।

দুর্বলতাও আছে, আর তা এই পর্বের হিসাবেই দেখা যায়: ছোট matrix-এ ভরা আর খালি হওয়ার সময়ে ব্যবহার কম (৩ × ৩-এ ৪২.৯%), আর matrix-এর আকার array-এর সাথে না মিললে কিছু PE অলস। তাই বড় systolic array বড় batch আর বড় স্তরে সবচেয়ে ভালো, আর আধুনিক নকশায় এর পাশে vector unit রাখা হয় অন্য কাজের জন্য।

মূল উপলব্ধি: একটি ধারণা টিকে থাকে কি না, তা ঠিক করে প্রযুক্তির অর্থনীতি; Dennard-এর অবসান আর AI-এর বিশাল একরৈখিক কাজ মিলে ৪০ বছরের পুরোনো systolic array-কে আবার সঠিক উত্তর বানিয়েছে।

প্র ০৪AI chip-এর বিজ্ঞাপনে হিসাবের সংখ্যা (TOPS, FLOPS) বড় করে দেখানো হয়, অথচ বড় ভাষা-model চালাতে প্রায়ই মেমরিই বাধা। কেন এমন, আর hardware নির্মাতারা কী করছেন?

Transistor ছোট হওয়ার সাথে সাথে chip-এর ভেতরে গুণক বসানো সস্তা হয়েছে — একই জায়গায় প্রতি প্রজন্মে আরও বেশি MAC. কিন্তু chip-এর বাইরে ডেটা আনার পথ সীমিত: package-এর pin-সংখ্যা, তারের দৈর্ঘ্য, আর প্রতি bit পাঠানোর energy. এই ফাঁককে বলে memory wall (পর্ব ৪৪), আর তা দশকের পর দশক বেড়েছে। ফলে হিসাবের চূড়ান্ত সংখ্যা যত দ্রুত বেড়েছে, মেমরির bandwidth তত দ্রুত বাড়েনি।

বড় ভাষা-model-এর inference এই ফাঁকের ঠিক মাঝখানে পড়ে। একজন ব্যবহারকারীর জন্য প্রতিটি token-এ সব weight একবার পড়া, প্রতিটিতে একটি গুণ-যোগ — arithmetic intensity প্রায় ১ FLOP/byte (FP16-এ)। পর্ব ৫৪-এর roofline-এ এটি একেবারে বাঁ দিকে, ঢালু অংশে: গতি ঠিক করে bandwidth, TOPS নয়। এই পর্বের হিসাবে ১ TB/s-এ ৭০০ কোটি parameter-এর FP16 model সর্বোচ্চ ≈ ৭১ token/s পায়, যদিও তার জন্য দরকার মাত্র ≈ ১ TFLOP/s.

নির্মাতারা কয়েক দিক থেকে আক্রমণ করছেন। মেমরি চওড়া করা: HBM — DRAM-এর কয়েকটি die খাড়া করে বসিয়ে হাজারের বেশি তারের চওড়া bus, processor-এর ঠিক পাশে একই package-এ। Byte কমানো: FP8, INT8, ৪-bit quantization — bit অর্ধেক মানে token/s প্রায় দ্বিগুণ, যদি নির্ভুলতা টিকে থাকে। পুনর্ব্যবহার বাড়ানো: server-এ অনেক অনুরোধ একসাথে batch করা, যাতে একবার পড়া weight অনেক token-এ লাগে। আর ডেটা কাছে রাখা: বড় on-chip SRAM, কিছু নকশায় পুরো model chip-গুলোর SRAM-এ ছড়িয়ে রাখা, আর গবেষণায় মেমরির ভেতরেই হিসাব (processing-in-memory).

ব্যবহারকারীর জন্য শিক্ষা: একটি laptop-এ কোন model কত দ্রুত চলবে, তা অনুমান করতে NPU-র TOPS নয়, মেমরির bandwidth আর model-এর আকার (bytes) দেখুন। বাংলাদেশে যাঁরা নিজের কম্পিউটারে ছোট model চালিয়ে পরীক্ষা করেন, তাঁরা প্রায়ই দেখেন quantized রূপ অনেক দ্রুত — কারণটি এই হিসাবেই।

মূল উপলব্ধি: হিসাব সস্তা, ডেটা সরানো দামি; AI hardware-এর বিবর্তনের বড় অংশ আসলে bytes-এর বিরুদ্ধে লড়াই — চওড়া মেমরি, কম bit আর বেশি পুনর্ব্যবহার।

প্র ০৫Bitcoin mining-এর ইতিহাসে hardware বদলেছে CPU → GPU → FPGA → ASIC ক্রমে। এই গল্প থেকে GPU, FPGA আর ASIC বেছে নেওয়ার নিয়ম সম্পর্কে কী শেখা যায়?

Bitcoin mining মূলত একটি কাজ বারবার: SHA-256 hash হিসাব করা, কোটি কোটি বার, ভিন্ন ভিন্ন সংখ্যায়। ২০০৯ সালের শুরুতে সাধারণ CPU-তেই এটি করা হতো। কাজটি পুরোপুরি data-parallel — প্রতিটি চেষ্টা অন্যটির থেকে স্বাধীন — আর শুধু integer bit-কাজ, শাখা প্রায় নেই। তাই কিছুদিনের মধ্যেই GPU-তে তা অনেক গুণ দ্রুত চলতে দেখা যায়: হাজার lane, কোনো divergence নেই, মেমরি প্রায় লাগে না। এরপর কেউ কেউ FPGA-তে hash circuit বসান — গতি GPU-র মতো বা কম, কিন্তু প্রতি watt-এ বেশি কাজ, আর mining-এ বিদ্যুৎ-বিলই আসল খরচ। শেষে, বাজার যথেষ্ট বড় হলে ২০১৩-র আশেপাশে থেকে শুধু SHA-256-এর জন্য ASIC আসে, যা প্রতি watt-এ আগের সবকিছুকে বহু গুণ ছাড়িয়ে যায়; তারপর থেকে সাধারণ hardware-এ Bitcoin mining প্রায় অর্থহীন।

এই ক্রমের পেছনে একটি সাধারণ নিয়ম আছে। নমনীয়তা আর দক্ষতার মধ্যে আদান-প্রদান: CPU সব পারে, কিন্তু প্রতি কাজে control আর অনুমানের খরচ বহন করে; GPU data-parallel কাজে সেই খরচ ভাগ করে নেয়; FPGA circuit নিজেই কাজের আকারে সাজায়, কিন্তু programmable তারের overhead রাখে; ASIC সেই overhead-ও মুছে দেয়। অন্য অক্ষটি অর্থনীতি: ASIC-এর নকশা, যাচাই আর উৎপাদন শুরুর খরচ বিশাল, তাই তখনই লাভ যখন কাজটি স্থির (অ্যালগরিদম বদলাবে না) আর বিশাল পরিমাণে চলবে। Bitcoin-এর hash অ্যালগরিদম বদলায় না — ASIC-এর জন্য আদর্শ।

AI-তে ছবিটি আরও জটিল, কারণ model-এর ধরন প্রতি কয়েক বছরে বদলায় — convolution থেকে transformer, নতুন precision, নতুন স্তর। তাই সম্পূর্ণ স্থির ASIC-এর ঝুঁকি বেশি; সফল AI ASIC (যেমন TPU) সাধারণত matrix গুণের মতো দীর্ঘস্থায়ী মূল কাজকে hardware-এ বাঁধে আর বাকিটা প্রোগ্রাম করা যায় এমন রাখে। GPU-ও উল্টো দিক থেকে কাছে এসেছে — tensor core বসিয়ে। FPGA টিকে আছে যেখানে latency খুব কম দরকার, পরিমাণ ছোট, বা নকশা ঘনঘন বদলায় — আর chip বানানোর আগে ASIC নকশা যাচাইয়ে।

মূল উপলব্ধি: কাজ যত স্থির আর যত বিশাল পরিমাণে, hardware তত বিশেষায়িত হওয়া লাভজনক; কাজ যত বদলায়, নমনীয়তার দাম তত বেশি।

অনুশীলন

পরীক্ষার ধাঁচের সমস্যা — কাগজ-কলমে আগে নিজে করুন, তারপর পূর্ণ সমাধান মিলিয়ে নিন।

  1. Divergence-এর দক্ষতা আর আউটপুট অনুমান: (ক) ৩২-thread warp-এ শাখার আগে-পরে মোট ৪টি সাধারণ instruction; if (tid % 4 == 0) পথে ১০টি, else-পথে ২টি instruction. Issue-সংখ্যা আর SIMD দক্ষতা কত? (খ) শর্তটি if ((tid / 32) % 4 == 0) হলে একটি warp-এর ভেতরে দক্ষতা কত? (গ) অংশ ৩-এর simulator-এ trip সব ১ করলে, আর [3, 0, 0, 0, 0, 0, 0, 0] করলে, দক্ষতা কত হবে — আগে অনুমান করুন, তারপর চালিয়ে দেখুন।

    (ক) tid % 4 == 0 → প্রতি warp-এ ৮টি thread if-পথে, ২৪টি else-পথে। Issue = ৪ + ১০ + ২ = ১৬. সক্রিয় lane-slot = ৪ × ৩২ + ১০ × ৮ + ২ × ২৪ = ১২৮ + ৮০ + ৪৮ = ২৫৬. দক্ষতা = ২৫৬ / (১৬ × ৩২) = ২৫৬ / ৫১২ = ৫০%.

    (খ) একটি warp-এর ৩২টি thread-এর tid / 32 একই, তাই পুরো warp একই দিকে যায় — divergence নেই, দক্ষতা ১০০%. If-পথের warp ১৪টি, else-পথের warp ৬টি instruction নেয়। কাজ একই, শুধু শর্তটি warp-এর সীমানা মেনে বসানো।

    (গ) সব ১: if/else অংশ ৬টি issue (৩২ lane-slot), লুপে একবার সবাই ঘোরে — PC 6, 7, 8 সবাই, আবার PC 6-এ সবাই বেরোয় (uniform), শেষে store: মোট ১১টি issue, ৭২ lane-slot — 81.8%. [3, 0, …]: লুপের ঘোরা প্রথম উদাহরণের মতোই ৩টি (সবচেয়ে ধীর lane ঠিক করে), কিন্তু প্রতিটিতে মাত্র একটি lane — ১৭টি issue, ৫৭ lane-slot — 41.9%. একটিমাত্র ধীর thread পুরো warp-কে আটকে রাখে।

  2. Coalescing-এর transaction গোনা: ৩২ thread, ১২৮-byte খণ্ড ধরে নিন। (ক) 4-byte float, thread t পড়ে a[t * s]; s = 1, 2, 32-এ কয়টি transaction আর দক্ষতা কত? (খ) 8-byte double, s = 1? (গ) 4-byte, s = 1, কিন্তু array-টি ৪ byte সরে শুরু (base 4)? (ঘ) Row-major float M[1024][1024]-এর একটি কলামের যোগফল প্রতিটি thread একটি সারির ঘর ধরে করলে (thread t পড়ে M[t][j]) কী হয়, আর কীভাবে ঠিক করবেন — index-এর কোড লিখুন।

    (ক) s = 1: ঠিকানা 0–124 → ১টি, ১০০%. s = 2: 0–248 → ২টি, ৫০%. s = 32: প্রতি thread ১২৮ byte দূরে → ৩২টি, ১২৮ / ৪০৯৬ ≈ ৩.১%.

    (খ) ৩২ × ৮ = ২৫৬ byte টানা → ২টি transaction, দক্ষতা ১০০% (সব দরকারি)।

    (গ) ঠিকানা 4–128; শেষটি পরের খণ্ডে → ২টি, ৫০%.

    (ঘ) পাশাপাশি thread-এর ঠিকানার ব্যবধান এক সারি = ১০২৪ × ৪ = ৪০৯৬ byte — প্রতিটি thread আলাদা খণ্ডে, ৩২টি transaction, stride 32-এর চেয়েও খারাপ ছড়ানো। ঠিক করার উপায়: thread-কে কলাম দিন, সারি নয় — thread t নিজের কলাম j = blockIdx.x * blockDim.x + threadIdx.x নিয়ে লুপে for (i = 0; i < 1024; i++) sum += M[i * 1024 + j]; চালায়। এখন একই মুহূর্তে warp-এর thread-রা M[i][j..j+31] পড়ে — পাশাপাশি ঠিকানা, ১টি transaction. প্রয়োজনে shared memory-তে tile এনে transpose করাও প্রচলিত।

  3. Occupancy: SM-এর ধরে নেওয়া সীমা: ৬৫,৫৩৬টি register, ৬৪ KB shared memory, সর্বোচ্চ ২,০৪৮ thread (৬৪ warp), ৩২টি block. Kernel: block-এ ২৫৬ thread, প্রতি thread ৪০টি register. (ক) Shared memory না লাগলে occupancy কত? (খ) প্রতি block ১২ KB shared memory লাগলে? (গ) ১০০% পেতে register আর shared memory-র সর্বোচ্চ কত হতে পারে?

    (ক) প্রতি block-এ ২৫৬ × ৪০ = ১০,২৪০ register → ⌊৬৫,৫৩৬ / ১০,২৪০⌋ = ⌊৬.৪⌋ = ৬টি block. Thread-সীমা ৮. তাই ৬টি block = ৪৮টি warp → ৭৫% (অংশ ৪-এর calculator-এর চতুর্থ সারি)।

    (খ) Shared memory-সীমা ⌊৬৪ / ১২⌋ = ৫টি block; register ৬, thread ৮ → সবচেয়ে ছোট ৫. ৫ × ৮ = ৪০টি warp → ৪০ / ৬৪ = ৬২.৫%.

    (গ) ১০০% মানে ৬৪টি warp = ৮টি block = ২,০৪৮ thread. Register: ৬৫,৫৩৬ / ২,০৪৮ = ৩২টি প্রতি thread পর্যন্ত। Shared memory: ৬৪ KB / ৮ = ৮ KB প্রতি block পর্যন্ত। (বাস্তব GPU-তে বরাদ্দের খণ্ডের জন্য সংখ্যা সামান্য কম হতে পারে।)

  4. Systolic cycle আর offload-এর সিদ্ধান্ত: (ক) Output-stationary n × n systolic array-তে একটি ৪ × ৪ গুণে কয় cycle, কয়টি MAC, গড় ব্যবহার কত? ২৫৬ × ২৫৬ array-তে একটি ২৫৬ × ২৫৬ গুণে কয় cycle? (খ) PCIe ২৫ GB/s, CPU ১০০ GFLOP/s, GPU ১০ TFLOP/s, প্রতি kernel ১০ µs launch খরচ ধরে n = ৫০ আর n = ২০০-এর float matrix গুণ GPU-তে পাঠানো উচিত কি না হিসাব করুন (৩টি matrix পাঠাতে হয়)।

    (ক) ৩n − ২ = ৩ × ৪ − ২ = ১০ cycle; MAC = n³ = ৬৪; ব্যবহার = ৬৪ / (১০ × ১৬) = ৪০%. ২৫৬ × ২৫৬-এ ৩ × ২৫৬ − ২ = ৭৬৬ cycle (ফল বের করা বাদে)। অনেক matrix পরপর চালালে ভরা-খালির সময় ঢাকা পড়ে।

    (খ) n = ৫০: CPU = ২ × ১,২৫,০০০ / ১০১১ = ২.৫ µs. GPU = ১২ × ২,৫০০ / (২.৫ × ১০১০) + ২,৫০,০০০ / ১০১৩ + ১০ µs = ১.২ + ০.০২৫ + ১০ = ১১.২২৫ µs — CPU-তেই রাখুন। n = ২০০: CPU = ১.৬ × ১০৭ / ১০১১ = ১৬০ µs. GPU = ৪,৮০,০০০ / (২.৫ × ১০১০) = ১৯.২ µs + ১.৬ µs + ১০ µs = ৩০.৮ µs — GPU প্রায় ৫.২ গুণ দ্রুত, পাঠানো উচিত। (এই অনুমানে break-even প্রায় n = ৮৯.)

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

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

ক্রমআলাদা GPU-র একটি সাধারণ CUDA প্রোগ্রামে vector add-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. cudaMalloc দিয়ে GPU-র মেমরিতে জায়গা নেওয়া
  2. cudaMemcpy দিয়ে input array host থেকে device-এ পাঠানো
  3. Block-সংখ্যা হিসাব করে <<<blocks, threads>>> দিয়ে kernel চালু করা
  4. প্রতিটি thread নিজের index বের করে, পাহারা মেনে একটি উপাদান যোগ করা
  5. ফল device থেকে host-এ ফেরত আনা
  6. cudaFree দিয়ে GPU-র মেমরি ছেড়ে দেওয়া

৩২-thread-এর একটি warp-এ ১৬টি thread একটি if-পথে (৬টি instruction) আর ১৬টি else-পথে (৬টি instruction) যায়; শাখার বাইরে কোনো instruction নেই। SIMD দক্ষতা কত?

  • ১০০%
  • ৫০%
  • ২৫%
  • ৭৫%

৩২টি thread-এর একটি warp 4-byte float পড়ে, thread t পড়ে a[32 * t]; array ১২৮-byte সীমানায় align করা আর মেমরি ১২৮-byte খণ্ডে আসে। কয়টি খণ্ড (transaction) লাগবে?

  • ১
  • ২
  • ৪
  • ৩২

একটি SM-এ ৬৫,৫৩৬টি register আর সর্বোচ্চ ৬৪টি warp (২,০৪৮ thread) ধরা হলো; shared memory-র সীমা নেই। ২৫৬-thread block-এর একটি kernel প্রতি thread ৬৪টি register নিলে occupancy কত?

  • ১০০%
  • ৭৫%
  • ৫০%
  • ২৫%

Skewed input-এর একটি output-stationary ৩ × ৩ systolic array দুটি ৩ × ৩ matrix গুণ করছে। শেষ গুণ-যোগ পর্যন্ত কয় cycle লাগে (ফল বের করা বাদে)?

  • ৭
  • ৩
  • ৯
  • ২৭

৭ × ১০৯ parameter-এর একটি model INT8-এ (প্রতি parameter ১ byte), মেমরির bandwidth ১ TB/s; batch = ১-এ প্রতি token-এ সব weight একবার পড়তে হয়। Token/s-এর মোটামুটি ঊর্ধ্বসীমা কত?

  • প্রায় ৭১
  • প্রায় ১৪৩
  • প্রায় ২৮৬
  • কয়েক হাজার, কারণ chip-এর TOPS অনেক

দুটি ১০৮-উপাদানের float array যোগ একবারই করতে হবে, ডেটা আছে CPU-র মেমরিতে। আলাদা GPU-তে পাঠালে প্রায়ই লাভ হয় না। প্রধান কারণ কী?

  • GPU float যোগ করতে পারে না
  • Warp divergence
  • PCIe-তে ডেটা পাঠানোর সময় হিসাবের লাভকে ছাড়িয়ে যায়
  • Occupancy খুব কম

একটি AI chip-এ ৪,০৯৬টি INT8 MAC আছে, clock ১ GHz, আর প্রতিটি MAC-কে ২টি operation ধরা হয়। এর চূড়ান্ত (peak) TOPS কত?

  • ৪.০৯৬
  • ২.৪৬
  • ১৬.৩৮৪
  • ৮.১৯২

শব্দকোষ

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

মনে রাখুন

  1. CPU latency কমায়, GPU throughput বাড়ায়: GPU সরল lane-এর বিশাল দল আর হাজার জীবিত thread দিয়ে মেমরির অপেক্ষা ঢাকে; kernel → grid → block → thread, hardware-এ ৩২-এর warp.
  2. SIMT আর divergence: warp একটি instruction চালায়, শাখায় mask আর reconvergence stack দিয়ে দুই পথ পালা করে; দক্ষতা = সক্রিয় lane-slot / (issue × warp-এর আকার), সময় = দুই পথের যোগফল।
  3. Occupancy = জীবিত warp / সর্বোচ্চ; register, shared memory আর thread-সীমার সবচেয়ে ছোটটি ঠিক করে — বেশি মানেই দ্রুত নয়।
  4. মেমরি: coalescing-এ stride 1 → ১টি খণ্ড, stride 32 → ৩২টি; shared memory-র tile দিয়ে পুনর্ব্যবহার, bank conflict এড়াতে padding; PCIe-র খরচ মিলিয়ে offload — arithmetic intensity দেখুন।
  5. AI accelerator: tensor core আর systolic array (n × n-এ ৩n − ২ cycle) ডেটা পুনর্ব্যবহার করে; কম precision বেশি MAC আর কম byte দেয়; TOPS চূড়ান্ত সংখ্যা মাত্র, আর inference-এর token/s ≤ bandwidth / (parameter × bytes).

পরের পর্বে: GPU আর AI chip-এর আলোচনা বারবার এসে ঠেকেছে watt আর energy-তে। পরের পর্বে power আর energy-র হিসাব, DVFS, আর ছোট microcontroller থেকে ফোনের SoC পর্যন্ত embedded সিস্টেমের নকশা।

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

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