Roofline Model — Compute না Memory Bandwidth, কোনটা আপনাকে থামাচ্ছে
The Roofline Model
একটাই log-log প্লট যা বলে দেয় আপনার program compute দিয়ে আটকাচ্ছে না memory bandwidth দিয়ে — আর ভুল দিকে optimize করলে কেন সময় নষ্ট হয় তা সংখ্যা দিয়ে দেখায়।
আগে এটা বুঝি
দুইজন engineer, একই function, একই deadline।
প্রথমজন এক সপ্তাহ ঢেলে দিলেন AVX-512 intrinsics লিখতে — hand-vectorized loop, prefetch hint, loop unrolling। ফলাফল: ৩% দ্রুত।
দ্বিতীয়জন এক ঘণ্টা ব্যয় করলেন শুধু loop-এর memory access pattern পুনর্বিন্যাস করতে — কোনো নতুন instruction নেই, শুধু data cache-এ থাকা অবস্থায় পুনর্ব্যবহার করা। ফলাফল: ৪ গুণ দ্রুত।
দুইজনই দক্ষ। দুইজনই সঠিকভাবে optimize করেছেন। তবু একজনের পুরো সপ্তাহ প্রায় অপচয় হলো। কেন?
Amdahl আর Gustafson-এর লেসনে আমরা দেখেছি speedup-এর একটা সিলিং আছে — serial fraction যতই ছোট করুন, সেটাই বাধা হয়ে দাঁড়ায়। এখানে একটা ভিন্ন অক্ষে একই ধরনের সিলিং আছে। প্রশ্নটা এবার “কত অংশ parallel করা যায়” নয় — প্রশ্নটা “আপনার প্রসেসরের কোন resource-টা আগে ফুরিয়ে যাচ্ছে — ALU, নাকি memory bus?”
প্রথম engineer-এর function memory bus দিয়ে আটকে ছিল। AVX-512 আরো বেশি ALU দিলো, কিন্তু data-ই তো পৌঁছাচ্ছিল না দ্রুত। দ্বিতীয় engineer সেই bus-এর উপর চাপ কমিয়েছেন — একই ALU, একই core, কিন্তু এখন data cache থেকে আসছে, DRAM থেকে নয়।
এই লেসনে আমরা একটা একক ছবি আঁকব যা এই প্রশ্নের উত্তর আগেই দিয়ে দেয়, hardware দেখেই — কোন optimization চেষ্টা করার আগেই। এটাকে বলে roofline model।
মূল ধারণা
Arithmetic Intensity — একটা সংখ্যা যা সব বলে দেয়
Roofline model-এর কেন্দ্রে একটাই সংখ্যা: arithmetic intensity (AI) — প্রতি byte data move করতে কতগুলো floating-point operation করা হচ্ছে।
একক: FLOPs/byte।
গুরুত্বপূর্ণ শব্দ — “DRAM থেকে move করা”। L1 cache-এ বসে থাকা data বারবার পড়লে সেটা কোনো নতুন byte “move” করে না — DRAM bandwidth একবারই খরচ হয়েছিল, বাকিটা বিনামূল্যে। এইজন্যই cache hierarchy লেসনের পুরো বিষয়বস্তু এখানে ফিরে আসে: data যতবার পারা যায় cache-এ রেখে পুনর্ব্যবহার করাই এই সংখ্যাটা বাড়ানোর একমাত্র উপায়।
দুইটা সিলিং
একটা processor-এর দুইটা হার্ড সীমা আছে:
১. Peak compute — সর্বোচ্চ কতগুলো FLOP প্রতি সেকেন্ডে করা
সম্ভব (GFLOPS)। এটা ALU সংখ্যা, SIMD width, FMA unit, আর clock
speed দিয়ে নির্ধারিত — একটা ধ্রুবক, AI-এর উপর নির্ভর করে না।
২. Peak memory bandwidth — সর্বোচ্চ কত byte/second DRAM থেকে আনা সম্ভব (GB/s)। ALU যতই দ্রুত হোক, data না পৌঁছালে সেটা বসে থাকবে।
দ্বিতীয় সমীকরণটা লক্ষ্য করুন — এখানেই AI ঢোকে। বেশি AI মানে
প্রতি byte-এ বেশি কাজ, তাই একই bandwidth দিয়ে বেশি GFLOPS।
দুইটা একসাথে:
এটাই সম্পূর্ণ roofline equation। বাকি সব এই একটা সমীকরণের ব্যাখ্যা।
ছবিতে — কেন এর নাম “roofline”
Log-log axis-এ (দুই অক্ষই log scale): x-axis = AI, y-axis =
attainable GFLOPS।
- Peak compute — একটা অনুভূমিক রেখা (AI-এর উপর নির্ভর করে না)
AI × Peak BW— একটা কোণাকার রেখা, slope 1 (log-log-এ, কারণlog(GFLOPS) = log(AI) + log(BW))
দুইটা রেখা একটা বিন্দুতে মেলে — ridge point। তার বামে কোণাকার রেখা নিচে, তাই সেটাই সীমা (memory-bound)। তার ডানে অনুভূমিক রেখা নিচে, তাই সেটাই সীমা (compute-bound)।
GFLOPS (log scale)
│
│ compute roof ─────────────────────
│ ╱
│ ╱ ← ridge point
│ ╱
│ ╱ (compute-bound অঞ্চল, ডানে)
│ ╱
│ ╱
│ memory ╱
│ roof ╱
│ (slope=1)
│ ╱
│ ╱ (memory-bound অঞ্চল, বামে)
│ ╱
└──────────────────────────────────────────────► AI (log scale)
কম AI বেশি AI
(byte-ভারী কাজ) (compute-ভারী কাজ)Ridge point-এর AI মান বের করা সহজ — দুইটা রেখা যেখানে সমান:
একটা সম্পূর্ণ সাংখ্যিক উদাহরণ
ধরা যাক আমাদের example machine — একটা আধুনিক ৮-core desktop CPU-র মতো কিছু:
- Peak compute (double precision):
400 GFLOPS(৮ core × ৩ GHz × ২টা FMA unit × ৪-wide SIMD × ২ FLOP/FMA ≈ ৪০০) - Peak memory bandwidth:
50 GB/s(dual-channel DDR, বাস্তব সংখ্যা)
এখন তিনটা বাস্তব kernel-কে এই মাপকাঠিতে ফেলি।
১. SAXPY (y[i] = a*x[i] + y[i]), double precision
প্রতি element: ১ multiply + ১ add = ২ FLOP। Byte: x[i] পড়া (৮B)
y[i]পড়া (৮B) +y[i]লেখা (৮B) = ২৪ byte।
0.083 \< 8 — গভীরভাবে memory-bound। Ceiling:
0.083 × 50 ≈ 4.2 GFLOPS — machine-এর ৪০০ GFLOPS ক্ষমতার
১%-ও ব্যবহার হচ্ছে না, আর AVX-512 দিয়ে ALU যতই দ্রুত করুন,
এই সিলিং নড়বে না।
২. Naive (unblocked) dense matrix multiply, বড় n
অ্যালগরিদম নিজে Θ(n³) FLOP করে, আর তাত্ত্বিকভাবে মাত্র Θ(n²)
data নিয়ে কাজ করে — তাই মনে হতে পারে AI n-এর সাথে বাড়বে।
কিন্তু loop order যদি cache-unfriendly হয় (naive i-j-k loop,
কোনো blocking নেই), আর matrix পুরো cache-এ না আঁটে, তাহলে দ্বিতীয়
matrix-এর প্রতিটা column বারবার DRAM থেকে reload হয় — কার্যত প্রতি
সারির জন্য একবার করে। ফলাফল: effective byte movement Θ(n³)-এর
কাছাকাছি চলে যায়, আর measured AI নেমে আসে ~0.5–1 FLOPs/byte-এ।
এখনো 8-এর নিচে — memory-bound, যদিও অ্যালগরিদমটা “computationally
heavy” মনে হয়।
৩. Tiled/blocked matrix multiply
Block size এমনভাবে বাছা যাতে A, B, C-এর একটা tile একসাথে L2 cache-এ আঁটে। এখন প্রতিটা loaded element cache-এ থাকা অবস্থায় বহুবার পুনর্ব্যবহার হয় — DRAM-এ ফিরতে হয় না।
Effective AI উঠে যায় ~20–40 FLOPs/byte-এ (block size-এর
উপর নির্ভরশীল)।
Ceiling এখন min(400, 30×50=1500) = 400 GFLOPS — পুরো compute
roof-এর নিচে, যা আগেরটার চেয়ে ১০ গুণেরও বেশি বেশি।
| Kernel | AI (FLOPs/byte) | Ridge (8)-এর তুলনায় | Region | Ceiling |
|---|---|---|---|---|
| SAXPY | 0.083 | অনেক নিচে | Memory-bound | 4.2 GFLOPS |
| Naive matmul | 0.75 | নিচে | Memory-bound | 37.5 GFLOPS |
| Tiled matmul | 30 | উপরে | Compute-bound | 400 GFLOPS |
এই টেবিলের সবচেয়ে গুরুত্বপূর্ণ পংক্তি ২ আর ৩ — একই algorithm
(matrix multiply), একই FLOP সংখ্যা, শুধু memory access পুনর্বিন্যাস
করেই ridge point-এর ভুল পাশ থেকে সঠিক পাশে চলে গেছে। এটাই এই
module-এর “Matrix Multiply Optimization” project-এর ঠিক কী ঘটছে —
naive → tiled ধাপে GFLOPS লাফ দেয় মূলত AI বাড়ার কারণে, SIMD-এর
কোনো ভূমিকা ছাড়াই। SIMD আর threading আসে পরের ধাপে, যখন আপনি
ইতিমধ্যে compute-bound অঞ্চলে — তখনই ALU-কে দ্রুত করাটা অর্থবহ হয়,
কারণ এখন ALU-ই বাধা।
একাধিক ছাদ — বাস্তবতা আরেকটু জটিল
সরল roofline-এ একটাই compute ছাদ আর একটাই memory ছাদ ধরা হয়েছে। বাস্তবে একাধিক আছে:
- Compute-এর ধাপ: scalar peak (SIMD ছাড়া) < SIMD peak (SIMD কিন্তু FMA ছাড়া) < SIMD+FMA peak। যদি আপনার code auto-vectorize না হয়, আপনি আসলে সবচেয়ে নিচের ছাদের নিচে আটকে আছেন — machine-এর “প্রকৃত” peak GFLOPS আপনার জন্য প্রাসঙ্গিকই না।
- Bandwidth-এর ধাপ: L1 bandwidth সবচেয়ে বেশি (~কয়েকশ GB/s প্রতি core), তারপর L2, L3, সবশেষে DRAM সবচেয়ে কম (~কয়েক দশ GB/s পুরো chip-এ ভাগ করে)। একটা “hierarchical roofline”-এ প্রতিটা cache level-এর জন্য আলাদা কোণাকার রেখা আঁকা হয় — cache hierarchy লেসনের ঠিক সেই সংখ্যাগুলো এখানে axis হয়ে ফিরে আসছে।
Empirical Roofline Toolkit (ERT)-এর মতো টুল এই সবগুলো ছাদ একসাথে মেপে একটা সম্পূর্ণ multi-roof ছবি বানায়।
Hierarchical roofline — “bound” আসলে কোন level-এর সাপেক্ষে
এতক্ষণ আমরা AI হিসাব করেছি শুধু DRAM bandwidth-এর সাপেক্ষে
— কারণ সেটাই সবচেয়ে ধীর, সবচেয়ে সাধারণ bottleneck। কিন্তু বাস্তবে
একটা kernel L3-এর সাপেক্ষে compute-bound হয়েও DRAM-এর সাপেক্ষে
memory-bound থাকতে পারে — দুইটা ভিন্ন প্রশ্ন, দুইটা ভিন্ন উত্তর।
ধরা যাক আমাদের example machine-এর প্রতিটা level-এর bandwidth (illustrative, বড়-থেকে-ছোট cache-এ যত কাছে তত বেশি bandwidth):
| Level | Bandwidth | Ridge AI (peak compute 400 GFLOPS-এর সাপেক্ষে) |
|---|---|---|
| L1 | ~২০০০ GB/s | 0.2 FLOPs/byte |
| L2 | ~৬০০ GB/s | 0.67 FLOPs/byte |
| L3 | ~১৫০ GB/s | 2.67 FLOPs/byte |
| DRAM | ৫০ GB/s | 8 FLOPs/byte |
একটা kernel-এর AI = 3 নিন। DRAM roofline (ridge 8)-এর
সাপেক্ষে এটা memory-bound। কিন্তু L3 roofline (ridge 2.67)-এর
সাপেক্ষে 3 > 2.67, তাই সেই কণিকায় এটা compute-bound — অর্থাৎ
যদি data-টা L3-এই থেকে যায় (DRAM পর্যন্ত না গিয়ে), kernel-টা
আসলে ALU দিয়েই সীমাবদ্ধ, bandwidth দিয়ে না।
এই পার্থক্যটাই ব্যাখ্যা করে ব্লকিং/টাইলিং-এর আসল কাজ — এটা
AI-এর numerator (FLOP) বদলায় না, এটা kernel-কে একটা নিচু,
দ্রুততর bandwidth ছাদের নিচে নিয়ে আসে (DRAM ছাদ থেকে L2/L3 ছাদে),
যেখানে একই AI-তেই “bound” হওয়ার সিদ্ধান্তটা উল্টে যায়।
Single-core বনাম aggregate roofline — core যোগ করলে ridge point সরে যায়
Misconception অংশে আমরা দেখব কেন থ্রেড বাড়ালে memory-bound কাজে কোনো লাভ হয় না — এখানে সংখ্যা দিয়ে দেখি ঠিক কেন।
একটা single core-এর ridge point ছিল 400/50 = 8 (পুরো chip-এর
peak ধরে)। কিন্তু যদি আমরা per-core হিসেবে হিসাব করি (৮
core-এর chip):
- Per-core peak compute:
400 / 8 = 50 GFLOPS - Per-core bandwidth (bandwidth পুরো chip-এ ভাগ হয়, তাই একক
core একা চালালে পুরো
50 GB/s-ই পেতে পারে, কিন্তু ৮টা core একসাথে চালালে গড়ে মাথাপিছু50/8 ≈ 6.25 GB/s):
| পরিস্থিতি | Compute/core | BW/core | Ridge AI |
|---|---|---|---|
| ১টা core একা চলছে | 50 GFLOPS | 50 GB/s | 1.0 |
| ৮টা core একসাথে চলছে | 50 GFLOPS | 6.25 GB/s | 8.0 |
অর্থাৎ, সব core ব্যবহার করলে effective ridge point ৮ গুণ ডানে
সরে যায় (1.0 থেকে 8.0)। একটা kernel যার AI = 2, একা একটা
core-এ চললে compute-bound (2 > 1.0) মনে হতে পারে, কিন্তু সব
core দিয়ে চালালে সেই একই kernel memory-bound হয়ে যায় (2 \< 8.0) — কারণ bandwidth-এর জন্য এখন সব core প্রতিযোগিতা করছে।
এই কারণেই memory-bound kernel-এ core সংখ্যা বাড়ানো throughput প্রায় বাড়ায়ই না — বাড়তি core শুধু একটা সংকীর্ণ bandwidth-এর জন্য বেশি প্রতিযোগী যোগ করে।
ভেতরে কী ঘটছে
Peak সংখ্যাগুলো কোথা থেকে আসে
Datasheet-এর সংখ্যা আর বাস্তব measured সংখ্যা প্রায়ই আলাদা। সঠিক roofline আঁকতে দুইটা microbenchmark দরকার।
Peak GFLOPS মাপা: একটা tight loop লিখুন যা কয়েকটা register-এই বারবার FMA করে — কোনো নতুন memory access নেই, তাই memory bandwidth এখানে সম্পূর্ণ অপ্রাসঙ্গিক। যা measured হয় সেটাই আপনার machine-এর আসল compute ceiling — datasheet-এর তাত্ত্বিক সংখ্যার চেয়ে প্রায়ই কম, কারণ thermal throttling, turbo boost না টেকা, বা compiler পুরোপুরি FMA ব্যবহার না করা।
Peak bandwidth মাপা: STREAM benchmark — খুব বড় array (cache-এর
চেয়ে অনেক বড়, তাই প্রতিটা access সত্যিই DRAM-এ যায়) দিয়ে চারটা
kernel চালানো হয় (Copy, Scale, Add, Triad)। এটাই এই
ক্ষেত্রের de facto standard — প্রায় প্রতিটা HPC cluster-এর
স্পেসিফিকেশনে STREAM number থাকে।
একটা kernel-এর AI বাস্তবে কীভাবে মাপা হয়
হাতে হিসাব (যেমন আমরা উপরে করেছি) একটা প্রথম অনুমান — কিন্তু compiler optimization, cache reuse pattern, আর alignment এসব বদলে দিতে পারে। বাস্তব পদ্ধতি:
- FLOP গণনা: hardware performance counter (Intel-এ
fp_arith_inst_retired) — কতগুলো floating-point instruction আসলে retire হলো তা সরাসরি count করে। পরের লেসনেperf statদিয়ে ঠিক এই ধরনের counter পড়ব। - Byte movement গণনা: memory controller-এর counter (uncore
counter) — DRAM-এ ঠিক কত byte read/write হলো তা সরাসরি মাপে।
সহজ approximation:
LLC-load-misses × cache line size (64B)।
Intel Advisor বা likwid-perfctr-এর মতো টুল এই দুইটা মাপ একসাথে
নিয়ে সরাসরি roofline plot-এ point বসিয়ে দেয় — নিজে হাতে হিসাব
করার দরকার পড়ে না।
স্বয়ংক্রিয় roofline টুল — একটা illustrative output
likwid-perfctr বা Intel Advisor-এর মতো টুল চালালে (illustrative
output):
$ likwid-perfctr -g FLOPS_DP -C 0 ./matmul_tiled
+-----------------------+----------+
| Metric | Core 0 |
+-----------------------+----------+
| Runtime (RDTSC) [s] | 0.842 |
| DP MFLOP/s | 182340.5 |
| AVX DP MFLOP/s | 175200.2 |
| Memory bandwidth [MB/s]| 6120.4 |
+-----------------------+----------+
Derived: Arithmetic Intensity = 182340.5 / 6120.4 ≈ 29.79 FLOPs/byteএই একটা output-এ hand-calculation-এর প্রতিটা ধাপ automated —
AI ≈ 29.79, যা আমাদের হাতে-হিসাব-করা ~30-এর সাথে চমৎকার মেলে।
পার্থক্যটুকু (30 বনাম 29.79) আসছে বাস্তব compiler-generated
code-এর সামান্য বাড়তি overhead (loop bookkeeping, address
calculation) থেকে, যা hand-calculation-এ ধরা পড়ে না।
এই মিলটাই গুরুত্বপূর্ণ শিক্ষা: hand-calculated AI একটা ভালো
প্রথম অনুমান হিসেবে কাজ করে (পরিকল্পনার জন্য যথেষ্ট), কিন্তু
চূড়ান্ত claim-এর জন্য সবসময় measured AI-ই বিশ্বাস করা উচিত —
ঠিক পরের দুইটা লেসনের কেন্দ্রীয় নীতি, যা এখানেই প্রথম প্রয়োগ
হলো।
উদাহরণ
SAXPY-এর AI হাতে হিসাব — একটা টেমপ্লেট
// SAXPY: y[i] = a*x[i] + y[i], n টা single-precision float
void saxpy(float a, float *x, float *y, int n) {
for (int i = 0; i \< n; i++) {
y[i] = a * x[i] + y[i];
}
}FLOP গণনা: প্রতি iteration ১ multiply + ১ add = ২ FLOP।
মোট: 2n FLOP।
Byte গণনা (single precision, 4 byte প্রতি float):
x[i] পড়া (4B) + y[i] পুরনো মান পড়া (4B) + y[i] নতুন মান
লেখা (4B) = 12 byte প্রতি iteration। মোট: 12n byte।
লক্ষ্য করুন — এটা আগের double-precision হিসাবের (0.083) ঠিক
দ্বিগুণ। কারণ single precision-এ প্রতিটা byte কম নেয় (4B বনাম
8B) কিন্তু FLOP সংখ্যা একই থাকে — তাই AI দ্বিগুণ হয়। এটাই কারণ
single-precision কাজ প্রায়ই double-precision-এর চেয়ে দ্রুত “মনে
হয়” — শুধু ALU-এর জন্য নয়, কম byte move করার জন্যও।
Ridge point-এর দুই পাশে একই algorithm
আরেকটা বাস্তব উদাহরণ — 7-point stencil (finite-difference PDE solver-এ সাধারণ, 3D grid-এর প্রতিটা point-এর ৬টা প্রতিবেশী আর নিজেকে নিয়ে একটা weighted sum):
out[i] = c0*in[i]
+ c1*(in[i-1] + in[i+1])
+ c2*(in[i-nx] + in[i+nx])
+ c3*(in[i-nx*ny] + in[i+nx*ny]);প্রতি point: ৭টা multiply + ৬টা add ≈ ১৩ FLOP।
Naive সংস্করণ (প্রতিটা প্রতিবেশী প্রতিবার DRAM থেকে ধরে নেওয়া, কোনো reuse নেই): ৭টা read (56B) + ১টা write (8B) = 64B।
Cache-aware সংস্করণ (adjacent point-এর মধ্যে প্রতিবেশী overlap হয় — একবার loaded value বহু point-এ পুনর্ব্যবহার করা, tiling দিয়ে):
দুইটাই ridge point (8)-এর অনেক নিচে — stencil সবসময়ই memory-bound
থাকে, matmul-এর মতো compute-bound হয়ে যায় না, যতই tile করুন।
পার্থক্য শুধু কতটা memory-bound — tiling ceiling 10 GFLOPS
থেকে 27 GFLOPS-এ তোলে, ভালো উন্নতি, কিন্তু matmul-এর মতো
১০ গুণ লাফ নয়।
শিক্ষা: roofline শুধু “bound না bound নয়” বলে না — এটা বলে tiling থেকে ঠিক কতটা লাভ আশা করা উচিত, optimization শুরু করার আগেই।
- Naive triple loopAI ≈ 0.75 — deep memory-bound, ceiling ~37 GFLOPS
- + Loop tiling/blockingAI ≈ 30 — ridge পার হয়ে compute-bound, ceiling ~400 GFLOPS
- + SIMD (AVX2/AVX-512)একই AI, কিন্তু achievable GFLOPS ঊর্ধ্বমুখী — ceiling-এর কাছে ওঠা
- + Multithreadingcompute roof নিজেই উঁচু হয় (single-core peak × core সংখ্যা)
লক্ষ্য করুন ক্রমটা — tiling প্রথমে, কারণ ridge point পার না হলে SIMD আর threading-এর কোনো প্রভাবই থাকবে না। এটাই সেই engineer-এর গল্প যেখানে ১ সপ্তাহের AVX-512 কাজ ৩% এনেছিল — তিনি ধাপ ২ না করেই ধাপ ৩-এ ঝাঁপ দিয়েছিলেন।
GPU-তে ridge point কেন অনেক ডানে সরে যায়
GPU computing (এই module-এরই একটা আগের বিষয়) hardware-এর দৃষ্টিতে একটা চরম উদাহরণ। ধরা যাক আমাদের example GPU-তে (Tensor Core-সহ আধুনিক GPU-র মতো, mixed-precision mode-এ):
- Peak compute (FP16, Tensor Core):
300000GFLOPS (300TFLOPS) - Peak memory bandwidth (HBM):
1500GB/s
আমাদের CPU example-এর ridge (8)-এর তুলনায় ২৫ গুণ ডানে।
এর মানে — আগের যে kernel-গুলো CPU-তে compute-bound ছিল (যেমন
tiled matmul, AI ≈ 30), সেই একই kernel GPU-তে 30 \< 200
হওয়ায় memory-bound হয়ে যায়!
এইজন্যই প্রায় প্রতিটা naive-ভাবে লেখা GPU kernel memory-bound
শুরু হয় — GPU-র বিশাল compute ক্ষমতা তুলনামূলকভাবে সংকীর্ণ
bandwidth-এর পাশে দাঁড় করালে ridge point এত দূরে সরে যায় যে
প্রায় কোনো বাস্তব kernel-ই সহজে সেটা পার হতে পারে না — যতক্ষণ না
shared memory tiling (GPU-র নিজস্ব cache blocking, parallel
patterns লেসনে আলোচিত) দিয়ে effective AI নাটকীয়ভাবে বাড়ানো হয়।
এই একটা সংখ্যাই ব্যাখ্যা করে কেন CUDA optimization guide-এর প্রথম
এবং সবচেয়ে গুরুত্বপূর্ণ পরামর্শ সবসময় data reuse, কখনো “আরো
thread যোগ করুন” না।
নিজে চালিয়ে দেখুন
AI বাড়ালে GFLOPS কীভাবে বাড়ে — নিজের হাতে roofline পুনরায় আঁকুন
মূল কৌশল: একটা array load করুন, তারপর সেই loaded value-টা নিয়ে
REPS বার FMA করুন register-এই (নতুন memory access ছাড়া)। REPS
বাড়ালে একই byte movement-এ বেশি FLOP হয় — অর্থাৎ AI কৃত্রিমভাবে
বাড়ানো যায়, ঠিক ERT (Empirical Roofline Toolkit) যা করে তার একটা
সরলীকৃত সংস্করণ।
#include \<stdio.h>
#include \<stdlib.h>
#include \<time.h>
#define N (64 * 1024 * 1024) // বড় — cache ছাড়িয়ে যায়
double bench(double *a, int reps) {
double sum = 0.0;
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
for (long i = 0; i \< N; i++) {
double v = a[i]; // ১টা load — এটাই একমাত্র memory access
for (int r = 0; r \< reps; r++) {
v = v * 1.0000001 + 0.0000001; // FMA, register-এই থাকে
}
sum += v;
}
clock_gettime(CLOCK_MONOTONIC, &t1);
double dt = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
double flops = (double)N * reps * 2.0; // প্রতি FMA = ২ FLOP
double bytes = (double)N * 8.0; // প্রতি element একবারই loaded
double ai = flops / bytes;
double gflops = flops / dt / 1e9;
printf("reps=%4d AI=%8.3f FLOPs/B time=%6.3fs GFLOPS=%7.2f\n",
reps, ai, dt, gflops);
if (sum == 123456789.0) printf(""); // optimize-away ঠেকাতে
return gflops;
}
int main(void) {
double *a = malloc(N * sizeof(double));
for (long i = 0; i \< N; i++) a[i] = 1.0;
int reps_list[] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512};
for (int k = 0; k \< 10; k++) bench(a, reps_list[k]);
free(a);
return 0;
}gcc -O2 -march=native -o roofline_sweep roofline_sweep.c
./roofline_sweepসাধারণ (illustrative) ফলাফল একটা memory-bound-থেকে-compute-bound-এ transition দেখায়:
reps= 1 AI= 0.250 FLOPs/B time= 0.410s GFLOPS= 3.27
reps= 2 AI= 0.500 FLOPs/B time= 0.412s GFLOPS= 6.51
reps= 4 AI= 1.000 FLOPs/B time= 0.415s GFLOPS= 12.93
reps= 8 AI= 2.000 FLOPs/B time= 0.421s GFLOPS= 25.53
reps= 16 AI= 4.000 FLOPs/B time= 0.433s GFLOPS= 49.63
reps= 32 AI= 8.000 FLOPs/B time= 0.460s GFLOPS= 93.39
reps= 64 AI= 16.000 FLOPs/B time= 0.610s GFLOPS= 140.90
reps= 128 AI= 32.000 FLOPs/B time= 0.920s GFLOPS= 186.98
reps= 256 AI= 64.000 FLOPs/B time= 1.520s GFLOPS= 226.32
reps= 512 AI= 128.000 FLOPs/B time= 2.750s GFLOPS= 250.15লক্ষ্য করুন প্যাটার্নটা: AI = 0.25 থেকে 8 পর্যন্ত GFLOPS
প্রায় হুবহু AI-এর সমানুপাতিক বাড়ছে (প্রতি দ্বিগুণে দ্বিগুণ) —
এটাই memory roof-এর slope-1 আচরণ। তারপর বৃদ্ধির হার কমতে শুরু করে,
আর শেষে প্রায় সমতল হয়ে যায় — এটাই compute roof-এর কাছে পৌঁছানো।
একটা সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ যাচাই: doubling ratio কোথায়
ভাঙতে শুরু করে দেখুন। AI=0.25→8 পর্যন্ত ratio প্রায় ঠিক 2.0×
(perfect linear)। কিন্তু AI=8→16-এ ratio নেমে আসে ~1.51×-এ,
আর তারপর ক্রমাগত কমতেই থাকে। এই ভাঙন-বিন্দুটা — AI ≈ 8-এর
কাছাকাছি — এই লেসনের প্রথমার্ধে হাতে হিসাব করা আমাদের example
machine-এর ridge point (400/50 = 8)-এর সাথে প্রায় হুবহু মিলে
যায়। এটা কাকতালীয় না — sweep experiment-টা আসলে হাতে-হিসাব করা
তাত্ত্বিক মডেলকেই পরীক্ষামূলকভাবে পুনরুৎপাদন করছে, দুইটা সম্পূর্ণ
স্বাধীন পদ্ধতিতে (একবার arithmetic দিয়ে, একবার measurement দিয়ে)
একই সিদ্ধান্তে পৌঁছে।
perf stat দিয়ে সরাসরি hardware counter থেকেও যাচাই করা যায়
(পরের লেসনে বিস্তারিত):
perf stat -e fp_arith_inst_retired.256b_packed_double,cycles ./roofline_sweepএকটা single microbenchmark-এ শুধু প্রতি loaded byte-এ কাজের পরিমাণ (AI) বাড়িয়ে দেখানো যায় GFLOPS প্রথমে রৈখিকভাবে বাড়ে (memory roof-এর slope অনুসরণ করে), তারপর একটা ধ্রুবকে plateau করে (compute roof) — ঠিক roofline model যা predict করে।
নিজে বানান
Roofline Plotter — hardware peak আর measured point থেকে স্বয়ংক্রিয় verdict
- Peak GFLOPS আর peak bandwidth থেকে ridge point AI হিসাব করুন
- Log-log axis-এ অনুভূমিক compute roof আর কোণাকার memory roof আঁকুন
- প্রতিটা measured kernel-এর (AI, GFLOPS) point বসান
- প্রতিটা point-এর জন্য স্বয়ংক্রিয়ভাবে compute-bound না memory-bound তা classify করুন, আর roof-এর কত শতাংশে পৌঁছেছে তা efficiency হিসেবে বের করুন
import math
def ridge_point(peak_gflops, peak_bw_gbs):
return peak_gflops / peak_bw_gbs
def classify(ai, gflops, peak_gflops, peak_bw_gbs):
"""একটা measured point নিয়ে verdict দেয়।"""
ridge = ridge_point(peak_gflops, peak_bw_gbs)
ceiling = min(peak_gflops, ai * peak_bw_gbs)
region = "compute-bound" if ai >= ridge else "memory-bound"
efficiency = gflops / ceiling * 100 if ceiling > 0 else 0.0
return {
"ridge_ai": ridge,
"ceiling_gflops": ceiling,
"region": region,
"efficiency_pct": efficiency,
}
def report(name, ai, gflops, peak_gflops, peak_bw_gbs):
r = classify(ai, gflops, peak_gflops, peak_bw_gbs)
print(f"\n{name}")
print(f" AI measured : {ai:.3f} FLOPs/byte")
print(f" GFLOPS measured : {gflops:.2f}")
print(f" Ridge point AI : {r['ridge_ai']:.2f} FLOPs/byte")
print(f" Region : {r['region']}")
print(f" Ceiling for AI : {r['ceiling_gflops']:.2f} GFLOPS")
print(f" Roof efficiency : {r['efficiency_pct']:.1f}%")
if r["region"] == "memory-bound":
print(" পরামর্শ : cache blocking/tiling দিয়ে AI বাড়ান — SIMD এখনো কাজে দেবে না")
else:
print(" পরামর্শ : SIMD/threading দিয়ে ceiling-এর কাছে পৌঁছান — AI ইতিমধ্যে যথেষ্ট")
PEAK_GFLOPS = 400.0
PEAK_BW = 50.0
report("SAXPY (double)", ai=0.083, gflops=3.9, peak_gflops=PEAK_GFLOPS, peak_bw_gbs=PEAK_BW)
report("Naive matmul", ai=0.75, gflops=30.0, peak_gflops=PEAK_GFLOPS, peak_bw_gbs=PEAK_BW)
report("Tiled matmul", ai=30.0, gflops=180.0, peak_gflops=PEAK_GFLOPS, peak_bw_gbs=PEAK_BW)
report("Tiled + SIMD + thread", ai=30.0, gflops=360.0, peak_gflops=PEAK_GFLOPS, peak_bw_gbs=PEAK_BW)প্রত্যাশিত আউটপুট (সংক্ষিপ্ত):
Tiled matmul
AI measured : 30.000 FLOPs/byte
GFLOPS measured : 180.00
Ridge point AI : 8.00 FLOPs/byte
Region : compute-bound
Ceiling for AI : 400.00 GFLOPS
Roof efficiency : 45.0%
পরামর্শ : SIMD/threading দিয়ে ceiling-এর কাছে পৌঁছান — AI ইতিমধ্যে যথেষ্ট
Tiled + SIMD + thread
AI measured : 30.000 FLOPs/byte
GFLOPS measured : 360.00
Region : compute-bound
Roof efficiency : 90.0%লক্ষ্য করুন শেষ দুইটা সারি — একই AI (tiling আগেই করা হয়েছে),
কিন্তু SIMD+threading যোগ করায় roof efficiency 45% থেকে 90%-এ
উঠেছে। এটাই compute-bound region-এ থাকা অবস্থায় সঠিক optimization
প্রয়োগের প্রভাব — যদি এই দুইটা kernel এখনো memory-bound region-এ
থাকত, gflops সংখ্যা দুইটা প্রায় সমানই থাকত, যতই SIMD ভালো হোক।
নিজে বাড়ান:
- একটা দ্বিতীয় হার্ডওয়্যার profile যোগ করুন (যেমন GPU-এর মতো বেশি peak GFLOPS কিন্তু আনুপাতিকভাবে কম bandwidth) — ridge point কীভাবে সরে যায় দেখুন
matplotlibদিয়ে সত্যিকার log-log plot আঁকুন —loglog()দিয়ে দুইটা roof আর point-গুলো visualize করুন- একাধিক bandwidth ছাদ যোগ করুন (L2, L3, DRAM) — hierarchical roofline বানান
Experimentঅংশেরroofline_sweep.c-এর প্রকৃত আউটপুট এই plotter-এ feed করুন — নিজের machine-এর roofline বাস্তবে আঁকুন
বাস্তব সিস্টেমে
Roofline যেখানে ব্যবহারিক সিদ্ধান্ত নেয়
BLAS library-গুলো (OpenBLAS, Intel MKL, BLIS)। GEMM (matrix
multiply)-এর প্রতিটা ভালো implementation-ই মূলত একটা roofline
optimization exercise — block size এমনভাবে বাছা হয় যাতে effective
AI ridge point পার হয়ে যায়, তারপর SIMD আর threading দিয়ে সেই
compute roof-এর ৯০%+ ছোঁয়া হয়। এই কারণেই hand-written GEMM কখনোই
এদের সাথে পাল্লা দিতে পারে না — tiling-এর জ্যামিতিটাই বেশিরভাগ কাজ।
Deep learning kernel। Convolution layer সাধারণত compute-bound
(একই weight বহুবার পুনর্ব্যবহার হয় — উচ্চ AI), তাই GPU-তে Tensor
Core-এর মতো বিশেষ compute unit যোগ করাটা লাভজনক। কিন্তু elementwise
op (ReLU, batch normalization, dropout) প্রায় সবসময় memory-bound —
প্রতি byte-এ মাত্র ১টা op। এই কারণেই XLA, TorchScript-এর মতো
compiler “operator fusion” করে — একাধিক elementwise op-কে একটা
single memory pass-এ মিশিয়ে দেয়, যাতে intermediate result বারবার
DRAM-এ যাওয়া-আসা না করে। এটা সরাসরি AI বাড়ানোর কৌশল।
GPU computing। GPU-র peak GFLOPS CPU-র চেয়ে বহুগুণ বেশি, কিন্তু bandwidth আনুপাতিক হারে ততটা বাড়েনি — তাই GPU-র ridge point CPU-র চেয়ে অনেক ডানে। ফলাফল: naive-ভাবে লেখা প্রায় প্রতিটা GPU kernel memory-bound, এমনকি সেই একই kernel CPU-তে compute-bound হতে পারে। এইজন্যই CUDA optimization guide-এর প্রথম পরামর্শই থাকে shared memory tiling — GPU-তে data reuse না করলে সেই বিশাল compute ক্ষমতার বেশিরভাগটাই অব্যবহৃত থেকে যায়।
Scientific computing / HPC centers। NERSC-এর মতো সুপারকম্পিউটিং কেন্দ্র Empirical Roofline Toolkit-কে standard triage টুল হিসেবে ব্যবহার করে — একটা application কোথায় সময় ব্যয় করছে সেটা GPU-hour বরাদ্দ করার আগে roofline দিয়ে classify করা হয়, যাতে ভুল দিকে optimization-এর সময় নষ্ট না হয়।
Vectorized query engine (DuckDB, ClickHouse)। Columnar scan আর aggregate query-গুলো সাধারণত deeply memory-bound — প্রতি byte-এ সামান্য কাজ (তুলনা, যোগ)। তাই এই engine-গুলোর optimization মূলত bandwidth-কেন্দ্রিক: compression (কম byte move), SIMD দিয়ে scan (compute-এ কোনো চাপ নেই বলেই সস্তা), আর column pruning (অপ্রয়োজনীয় byte একেবারেই না পড়া)।
Compiler optimization report। GCC/Clang-এর -fopt-info-vec
বা Intel-এর vectorization report কখনো কখনো সতর্ক করে যে একটা loop
vectorize হয়েছে ঠিকই, কিন্তু memory-bound হওয়ায় বাস্তব speedup
সীমিত — মূলত একটা mini roofline analysis, compile-time-এই।
Cloud instance নির্বাচন। Compute-optimized (উচ্চ GFLOPS/vCPU)
বনাম memory-optimized (উচ্চ bandwidth) instance বাছাই আসলে একটা
roofline প্রশ্ন — আপনার workload-এর AI না জেনে ভুল ধরনের (আর
প্রায়ই বেশি দামী) instance কেনাটা এখানে খুবই সাধারণ একটা ভুল।
Video/audio codec ডিজাইন। Transform-ভিত্তিক ধাপ (DCT, motion
compensation) সাধারণত compute-heavy, উচ্চ AI — বলেই hardware
video encoder chip-এ dedicated ALU যোগ করা লাভজনক। কিন্তু
entropy coding (Huffman/arithmetic coding-এর মতো bit-level কাজ)
সহজাতভাবে sequential আর memory-bound — সেখানে বাড়তি ALU যোগ করাটা
codec ডিজাইনারদের কাছে বহু বছর ধরে একটা পরিচিত dead-end, ঠিক এই
লেসনের roofline যুক্তিতেই।
CPU/SoC hardware ডিজাইন সিদ্ধান্ত। একটা নতুন CPU generation
ডিজাইন করার সময় architect-রা প্রশ্ন করেন — বাড়তি silicon budget
আরো ALU-তে দেব, নাকি আরো cache/wider memory bus-এ? এই সিদ্ধান্ত
নেওয়া হয় target workload-এর measured AI distribution দেখে —
যদি target workload বেশিরভাগই memory-bound (যেমন mobile SoC-এর
সাধারণ app workload), বাড়তি ALU-তে silicon ঢালা অপচয়; সার্ভার-গ্রেড
HPC chip-এ উল্টো সিদ্ধান্ত সঠিক হতে পারে। Roofline model তাই শুধু
software optimization-এর টুল না — hardware ডিজাইনের ভাষাও।
যে ভুলগুলো সবাই করে
“SIMD/vectorization সবসময় program দ্রুত করে।”
শুধু তখনই, যদি আপনি ইতিমধ্যে compute-bound হন।
উপরের SAXPY উদাহরণে ceiling ছিল 4.2 GFLOPS — memory bandwidth
দিয়ে নির্ধারিত। Scalar কোড আর হাতে-লেখা AVX-512 কোড দুইটাই এই
একই ceiling-এ গিয়ে থামবে, কারণ bottleneck ALU নয়, DRAM bus।
Scalar SAXPY : 4.1 GFLOPS
AVX2 SAXPY : 4.2 GFLOPS
AVX-512 SAXPY : 4.2 GFLOPSতিনটাই কার্যত সমান — কারণ ALU যতই দ্রুত হোক, memory bus একই গতিতে byte সরবরাহ করছে। SIMD-এর পুরো লাভ memory bus-এর জন্য অপেক্ষায় নষ্ট হয়ে যাচ্ছে।
নিয়ম: SIMD চেষ্টা করার আগে জানুন আপনি ridge point-এর কোন পাশে — নাহলে সপ্তাহভর কাজ ৩% ফলাফল দিতে পারে, যেমন এই লেসনের শুরুর গল্পে।
“বেশি core মানেই বেশি speed।”
Compute-bound কাজে হ্যাঁ — compute roof নিজেই উঁচু হয়। কিন্তু memory-bound কাজে না, কারণ সব core একই shared memory bus-এর জন্য প্রতিযোগিতা করে।
৮টা core একসাথে SAXPY চালালে মোট FLOP চাহিদা ৮ গুণ বাড়ে, কিন্তু DRAM bandwidth-টা সব core মিলে ভাগ করে নিতে হয় — একই ৫০ GB/s। ফলাফল: aggregate GFLOPS প্রায় একই থাকে যা single core একাই পেত, বরং cache line contention আর memory controller queueing-এর কারণে কখনো কখনো কমেও যায়।
এটা সরাসরি NUMA লেসনের বিষয়বস্তুর সাথে যুক্ত — একাধিক core যখন একই memory channel-এর জন্য লড়াই করে, তখন “বেশি thread = বেশি throughput” ধারণাটা ভেঙে পড়ে।
নিয়ম: memory-bound কাজে threading যোগ করার আগে দেখুন bandwidth
ইতিমধ্যে saturated কি না — perf stat-এ memory bandwidth counter
দেখে (পরের লেসন) এটা সরাসরি যাচাই করা যায়।
“Higher peak GFLOPS spec-এর CPU সবসময় দ্রুত।”
শুধু compute-bound কাজে। Marketing spec-এ “২ গুণ বেশি GFLOPS” লেখা একটা নতুন CPU কিনলে, যদি আপনার workload memory-bound হয় আর নতুন CPU-এর bandwidth পুরনোটার সমানই থাকে, তাহলে বাস্তব speedup প্রায় শূন্য।
Roofline সমীকরণে ফিরে যান: Attainable = min(Peak GFLOPS, AI × BW)।
AI নিচু থাকলে দ্বিতীয় term-ই সীমা নির্ধারণ করে — প্রথম term
(peak GFLOPS) দ্বিগুণ করলেও min()-এর ফলাফল বদলায় না।
নিয়ম: hardware কেনার আগে workload-এর AI জানুন। বেশিরভাগ
সাধারণ, non-numerical কাজ (string processing, JSON parsing, ORM
query) আসলে memory-bound — সেখানে বাড়তি ALU টাকা নষ্ট।
“Arithmetic intensity একটা algorithm-এর ধ্রুব বৈশিষ্ট্য — implementation বদলালে বদলায় না।”
সরাসরি ভুল, আর এই লেসনের সবচেয়ে গুরুত্বপূর্ণ পয়েন্ট। Naive matmul
আর tiled matmul — হুবহু একই algorithm, একই FLOP সংখ্যা
(2n³), একই output। তবু আমরা দেখেছি AI 0.75-এ থেকে 30-এ
লাফ দিল শুধু loop-এর memory access পুনর্বিন্যাস করে।
AI আসলে algorithm-এর property নয় — এটা নির্দিষ্ট
implementation-এর data reuse pattern-এর property। একই algorithm,
ভিন্ন cache blocking strategy, সম্পূর্ণ ভিন্ন AI।
এটাই asymptotic complexity-এর সাথে সবচেয়ে বড় পার্থক্য — Θ(n³)
লেখাটা কখনো বদলায় না loop order যাই হোক, কিন্তু AI বদলায়।
Level 0-এর asymptotic notation লেসনে আমরা দেখেছিলাম complexity
class-এ memory hierarchy অদৃশ্য — roofline সেই অদৃশ্য অংশটাকেই
একটা পরিমাপযোগ্য সংখ্যায় রূপান্তরিত করে।
“Roofline model শুধু HPC/scientific computing-এর জন্য প্রাসঙ্গিক — সাধারণ software-এ এর কোনো ব্যবহার নেই।”
এই লেসনের realworld অংশেই এর বিপরীত প্রমাণ ছড়িয়ে আছে — vectorized database query engine, deep learning inference server, cloud instance নির্বাচন, এমনকি video codec ডিজাইন। এর কোনোটাই সনাতন অর্থে “scientific computing” না।
ভুল ধারণাটার উৎস — roofline model-এর জন্ম হয়েছিল HPC community-তে (Williams et al. একটা supercomputing lab-এর গবেষক দল), আর “FLOPs”/“GFLOPS” শব্দগুলো শুনতে numerical computing-নির্দিষ্ট মনে হয়।
কিন্তু মূল ধারণাটা — arithmetic intensity বনাম hardware-এর দুইটা সিলিং — যেকোনো compute-এর জন্যই প্রযোজ্য, শুধু floating-point না। একটা string-processing loop-এর জন্য “FLOP” এর বদলে “operation” (তুলনা, hash, byte-copy) গুনে ঠিক একই বিশ্লেষণ করা যায় — এই কারণেই মূল paper-এ “operational intensity” নামটাও প্রস্তাবিত হয়েছিল।
নিয়ম: কোনো CPU-bound কাজ যেখানে memory access pattern নিয়ে প্রশ্ন উঠছে — সেটা matrix multiply হোক বা JSON parser — roofline lens প্রযোজ্য। নাম “FLOPs” শুনে ভয় পেয়ে এড়িয়ে যাওয়ার দরকার নেই।
বুঝেছেন কি না দেখুন
1একটা CPU-র peak compute 400 GFLOPS আর peak bandwidth 50 GB/s।
একটা kernel-এর measured AI = 2 FLOPs/byte। Ridge point কত?
Kernel-টা কোন region-এ, আর তার performance ceiling কত?
প্রয়োগ
400 GFLOPS আর peak bandwidth 50 GB/s।
একটা kernel-এর measured AI = 2 FLOPs/byte। Ridge point কত?
Kernel-টা কোন region-এ, আর তার performance ceiling কত?Ridge point:
Region: AI = 2, ridge AI = 8, আর 2 \< 8 — kernel-টা ridge point-এর
বামে, তাই memory-bound।
Ceiling:
এই kernel কখনোই 100 GFLOPS-এর বেশি করতে পারবে না, machine-এর
400 GFLOPS ক্ষমতা যাই থাকুক। এই সিলিং ভাঙতে হলে AI বাড়াতে
হবে (আরো cache reuse), শুধু ALU দ্রুত করে লাভ নেই।
2Naive matmul আর tiled matmul-এর FLOP সংখ্যা হুবহু সমান
(2n³)। তাহলে tiling কীভাবে AI বাড়ায় — numerator তো একই থেকে
যাচ্ছে?
যুক্তি
2n³)। তাহলে tiling কীভাবে AI বাড়ায় — numerator তো একই থেকে
যাচ্ছে?AI = FLOPs / bytes moved from DRAM — numerator (FLOPs) সত্যিই
একই থাকে, tiling সেটা বদলায় না। Denominator বদলায় — tiling
কমায় DRAM থেকে কত byte আনতে হয়, FLOP সংখ্যা নয়।
Naive i-j-k loop order-এ, matrix B পুরোপুরি cache-এ না আঁটলে,
প্রতিটা সারির জন্য পুরো B আবার DRAM থেকে পড়তে হয় — একই byte বহুবার
DRAM থেকে “move” হিসেবে গণ্য হয় (যদিও যুক্তিগতভাবে একই data)।
ফলে denominator বড় — কার্যত Θ(n³)-এর কাছাকাছি, Θ(n²) নয়।
Tiling-এ একটা block (যেমন 64×64) একবার cache-এ load হয়ে বহু
FLOP-এর জন্য পুনর্ব্যবহৃত হয়, তারপরই cache থেকে সরে যায়। DRAM
থেকে সেই একই data মাত্র একবার (বা কয়েকবার) আনা হয়, ব্লক আকারের
সমানুপাতিক ভাগে। denominator নাটকীয়ভাবে ছোট হয়।
সংক্ষেপে: AI মাপে reuse, শুধু কাজের পরিমাণ নয়। একই কাজ,
কম DRAM trip = বেশি AI। এটাই cache hierarchy লেসনের “locality of
reference” ধারণার একটা পরিমাপযোগ্য রূপ।
3SAXPY double precision-এ AI = 0.083। একই SAXPY single precision-এ
লিখলে AI ≈ 0.167 — ঠিক দ্বিগুণ। কেন?
প্রয়োগ
AI = 0.083। একই SAXPY single precision-এ
লিখলে AI ≈ 0.167 — ঠিক দ্বিগুণ। কেন?FLOP সংখ্যা অপরিবর্তিত — প্রতি element এখনো ১ multiply + ১ add =
2 FLOP, precision তাতে প্রভাব ফেলে না।
কিন্তু byte সংখ্যা অর্ধেক হয়ে যায় — double = 8 byte, float =
4 byte। একই তিনটা memory access (দুই read + এক write) এখন
3 × 4 = 12 byte নেয়, 3 × 8 = 24 byte-এর বদলে।
0.167 = 2 \times 0.083 — হুবহু দ্বিগুণ, কারণ numerator একই,
denominator অর্ধেক।
ব্যবহারিক তাৎপর্য: এই কারণেই memory-bound কাজে single precision-এ নামা (যেখানে accuracy-তে ছাড় দেওয়া যায়, যেমন অনেক machine learning inference) সরাসরি roofline ceiling দ্বিগুণ করে দেয় — ALU-তে কোনো পরিবর্তন ছাড়াই। GPU-তে FP16/bfloat16-এ নামলে একই যুক্তিতে ceiling আরো দ্বিগুণ হয়।
4একটা memory-bound kernel-এ core সংখ্যা ১ থেকে ৮-এ বাড়ালে GFLOPS
প্রায় একই থাকে, এমনকি সামান্য কমেও যেতে পারে। ব্যাখ্যা করুন কেন,
NUMA-এর ধারণা ব্যবহার করে।
যুক্তি
Roofline সমীকরণে AI × Peak BW হলো memory-bound region-এর ceiling।
এই Peak BW একটা shared resource — DRAM channel-এর bandwidth,
যা সব core একসাথে ভাগ করে নেয়, প্রতি core-এর জন্য আলাদা বরাদ্দ নয়।
১টা core একাই 50 GB/s-এর পুরোটা ব্যবহার করতে পারলে, ৮টা core
মিলেও সেই একই 50 GB/s-এর বেশি পাবে না — প্রতিটা core গড়ে
50/8 ≈ 6.25 GB/s পায় যদি সমানভাবে ভাগ হয়।
যেখান থেকে সামান্য কমে যাওয়া আসে (NUMA): যদি core-গুলো বিভিন্ন NUMA node-এ থাকে আর data একটামাত্র node-এর memory-তে বরাদ্দ করা থাকে, তাহলে remote node-এর core-গুলো একটা ভিন্ন, ধীরতর interconnect (যেমন QPI/Infinity Fabric) দিয়ে data আনে — effective bandwidth স্থানীয় memory bandwidth-এর চেয়ে কম। এছাড়া memory controller-এ contention (একাধিক core একসাথে request পাঠানো) queueing latency বাড়ায়, যা কিছুটা throughput নষ্ট করে।
সংক্ষেপে: memory-bound কাজে threading যোগ করা compute roof-কে প্রভাবিত করে (যেটা এখানে relevant না), memory roof-কে না — বরং NUMA-misplacement বাস্তবে memory roof-কেই সামান্য নিচে টেনে আনতে পারে।
5একটা program measured AI = 6 FLOPs/byte, machine-এর ridge point
8। Engineer speedup চান। দুইটা সম্ভাব্য কৌশল — (ক) AI বাড়ানো
(আরো cache reuse), (খ) SIMD দিয়ে achievable GFLOPS বাড়ানো — কোনটা
আগে চেষ্টা করা উচিত, আর কেন?
ডিজাইন
AI = 6 FLOPs/byte, machine-এর ridge point
8। Engineer speedup চান। দুইটা সম্ভাব্য কৌশল — (ক) AI বাড়ানো
(আরো cache reuse), (খ) SIMD দিয়ে achievable GFLOPS বাড়ানো — কোনটা
আগে চেষ্টা করা উচিত, আর কেন?AI = 6 \< 8 = ridge — program এখনো (সামান্য) memory-bound।
তাই প্রথমে (ক) — AI বাড়ানো — বেছে নেওয়া উচিত।
কারণ: memory-bound region-এ ceiling = AI × BW। এখানে
ceiling = 6 × 50 = 300 GFLOPS। SIMD দিয়ে ALU দ্রুত করলেও এই
ceiling-এর উপরে ওঠা যাবে না — memory bus এখনো bottleneck।
যদি AI-কে 8-এর সামান্য উপরে তোলা যায় (যেমন সামান্য বেশি
tiling/blocking, বা দুইটা pass-কে এক pass-এ fuse করা), program
ridge point পার হয়ে compute-bound region-এ ঢোকে — তখনই ceiling
400 GFLOPS-এ jump করে, আর SIMD/threading সেখানে বাস্তব লাভ
দেবে।
সংখ্যায় তুলনা:
| কৌশল | নতুন AI | নতুন ceiling |
|---|---|---|
| (খ) শুধু SIMD, AI অপরিবর্তিত | 6 | 300 GFLOPS (অপরিবর্তিত) |
| (ক) সামান্য tiling, AI → 10 | 10 | 400 GFLOPS |
AI মাত্র 6 থেকে 10-এ তোলাই ceiling-কে 300-থেকে 400-এ
তোলে (আর তারপর SIMD সেই নতুন, উঁচু ceiling-এর কাছে পৌঁছাতে সাহায্য
করে)। তাই order সবসময় প্রথমে AI, তারপর SIMD/threading —
বিশেষত যখন measured point ridge-এর কাছাকাছি থাকে, যেখানে সামান্য
পরিবর্তনই পাশ বদলে দিতে পারে।
সাধারণ নীতি: কখনোই SIMD/vectorization-এ সময় ঢালার আগে measure করে নিশ্চিত হোন আপনি ইতিমধ্যে ridge point-এর ডানে আছেন।
6আমাদের example GPU-এর (peak 300,000 GFLOPS, bandwidth 1500 GB/s) ridge point 200 FLOPs/byte। একটা tiled matmul kernel-এর
AI = 30 (যা CPU-তে compute-bound ছিল)। এই একই kernel GPU-তে
কোন region-এ পড়বে, আর engineer-এর পরবর্তী পদক্ষেপ কী হওয়া উচিত?
প্রয়োগ
300,000 GFLOPS, bandwidth 1500 GB/s) ridge point 200 FLOPs/byte। একটা tiled matmul kernel-এর
AI = 30 (যা CPU-তে compute-bound ছিল)। এই একই kernel GPU-তে
কোন region-এ পড়বে, আর engineer-এর পরবর্তী পদক্ষেপ কী হওয়া উচিত?GPU-তে ridge AI = 200, আর kernel-এর AI = 30, তাই 30 \< 200
— এই kernel GPU-তে memory-bound, যদিও ঠিক একই AI CPU-তে
(ridge 8) compute-bound ছিল (30 > 8)।
Ceiling: min(300000, 30 × 1500) = min(300000, 45000) = 45000 GFLOPS — GPU-র সম্পূর্ণ 300 TFLOPS ক্ষমতার মাত্র 15%।
পরবর্তী পদক্ষেপ — আরো tiling/blocking, GPU-নির্দিষ্ট কৌশলে:
GPU-তে এটা করা হয় shared memory-তে (একটা programmer-controlled,
on-chip, দ্রুত scratchpad — parallel patterns লেসনের বিষয়বস্তু)
data বসিয়ে, বহু thread দিয়ে পুনর্ব্যবহার করে, যাতে DRAM/HBM trip
কমে effective AI 200-এর উপরে ওঠে। শুধু thread সংখ্যা বাড়ানো
(compute বাড়ানো) কোনো কাজে দেবে না — এই kernel ইতিমধ্যে
memory-bound, ঠিক যেমন SAXPY-তে SIMD কাজ করেনি।
মূল শিক্ষা: একটা kernel “compute-bound” না “memory-bound” তা
hardware-নির্ভর — একই কোড, একই AI, ভিন্ন hardware-এ ভিন্ন
রায় দিতে পারে। Roofline classification কখনো portable ধরে নেওয়া
উচিত না — প্রতিটা টার্গেট hardware-এর জন্য আলাদাভাবে যাচাই করা
দরকার।
এরপর কী
এরপর — মডেল থেকে measurement-এ
এই লেসনে আমরা একটা তাত্ত্বিক ছবি বানিয়েছি: hardware-এর দুইটা
সিলিং, একটা kernel-এর AI হাতে হিসাব, আর সেই দুইয়ের সংযোগ থেকে
একটা predicted ceiling।
কিন্তু হাতে হিসাব করা AI অনুমান মাত্র — compiler কী optimize
করল, cache আসলে কতটা reuse হলো, memory controller কতটা efficient
ছিল, এসব হাত দিয়ে গোনা যায় না। পরের লেসনে আমরা শিখব কীভাবে
সত্যিকার hardware থেকে সত্যিকার সংখ্যা বের করা যায় — instructions
retired, cycles, cache miss, branch misprediction, আসল FLOP গণনা —
সরাসরি CPU-র ভেতরের Performance Monitoring Unit (PMU) থেকে,
perf টুল দিয়ে।
Roofline আপনাকে বলে দিয়েছে কী মাপতে হবে, আর একটা measured point কোথায় “থাকা উচিত” তার প্রত্যাশা তৈরি করেছে। এখন দরকার সেই point-টা বাস্তবে কোথায় তা নির্ভরযোগ্যভাবে বের করার হাতিয়ার।
আরও পড়ুন
- Roofline: An Insightful Visual Performance Model for Multicore Architectures — Samuel Williams, Andrew Waterman, David Patterson · Communications of the ACM, 2009 — মূল paper যেখানে roofline model প্রথম প্রকাশিত হয়
- STREAM: Sustainable Memory Bandwidth in High Performance Computers — John D. McCalpin · Peak memory bandwidth মাপার de facto standard microbenchmark
- Empirical Roofline Toolkit (ERT) — NERSC / Lawrence Berkeley National Laboratory · Theoretical নয়, বাস্তব measured roofline আঁকার automated টুল