পাঠ ৫৩ · ৫৭-এর মধ্যে · মডিউল ১১
Home / Courses / Computer Architecture & Digital Logic / GPU আর্কিটেকচার

GPU আর্কিটেকচার বেসিকস

GPU architecture basics
৮ মিনিট পড়া উন্নত · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • GPU কেন এবং কীভাবে গ্রাফিক্স রেন্ডারিং থেকে জন্ম নিয়েছিল
  • CPU ও GPU-এর মৌলিক আর্কিটেকচারাল দর্শন-পার্থক্য — অল্প জটিল কোর বনাম বিপুল সরল কোর
  • GPGPU ধারণা ও কেন এটি AI/ML-এ এত গুরুত্বপূর্ণ হয়ে উঠেছে
  • Python দিয়ে CPU-স্টাইল বনাম GPU-স্টাইল প্রসেসিং সিমুলেট করে বাস্তব সাইকেল-সংখ্যার পার্থক্য দেখা

১ · GPU কী এবং কেন তৈরি হয়েছিল

একটি ছবি রেন্ডার করতে স্ক্রিনের প্রতিটি পিক্সেলের রং আলাদাভাবে হিসাব করতে হয় — একটি ১০৮০p স্ক্রিনে প্রায় ২০ লাখেরও বেশি পিক্সেল! গুরুত্বপূর্ণ ব্যাপার হলো, প্রতিটি পিক্সেলের হিসাব বেশিরভাগ ক্ষেত্রে একে অপরের থেকে স্বাধীন এবং একই ধরনের গাণিতিক অপারেশন (একই "শেডার" কোড) দিয়ে করা হয় — এটাই M11/L50-এ শেখা SIMDSingle Instruction, Multiple Dataএকটি ইনস্ট্রাকশন একসাথে একাধিক ডেটার ওপর প্রয়োগ হয়। মডেলের একটি চরম, বাস্তব-জগতের উদাহরণ — GPU ঠিক এই ধরনের বিপুল-স্কেল, ডেটা-প্যারালাল কাজের জন্যই ডিজাইন করা হয়েছিল।

২ · CPU বনাম GPU — মৌলিক ডিজাইন-দর্শনের পার্থক্য

CPU
অল্প সংখ্যক (৪-৬৪টি) শক্তিশালী, জটিল কোর — প্রতিটি কোরে বড় ক্যাশ, শাখা-প্রেডিকশন (M7/L35), আউট-অফ-অর্ডার এক্সিকিউশনের মতো সফিস্টিকেটেড কন্ট্রোল লজিক — বৈচিত্র্যময় ইনস্ট্রাকশন স্ট্রিম দ্রুত সিকোয়েনশিয়ালি চালাতে অপটিমাইজড।
GPU
হাজার হাজার সরল, কম-শক্তিশালী কোর — ন্যূনতম কন্ট্রোল লজিক — একই ইনস্ট্রাকশন বহু ডেটা এলিমেন্টের ওপর একসাথে চালানোর জন্য অপটিমাইজড, প্রতি-কোর সফিস্টিকেশন বিসর্জন দিয়ে বিপুল প্যারালাল থ্রুপুট অর্জন করে।

সংক্ষেপে: CPU জেতে যখন কাজ জটিল, শাখাযুক্ত, একে অপরের ওপর নির্ভরশীল — GPU জেতে যখন কাজ সরল কিন্তু বিপুল পরিমাণে, স্বাধীনভাবে পুনরাবৃত্ত হয়।

৩ · GPGPU — গ্রাফিক্সের বাইরেও GPU

যেহেতু GPU মূলত "একই সরল অপারেশন বিপুল ডেটার ওপর" চালাতে দক্ষ, প্রকৌশলীরা লক্ষ্য করলেন এই একই ক্ষমতা গ্রাফিক্সের বাইরে অন্য অনেক কাজেও কাজে লাগে — যেমন ম্যাট্রিক্স গুণন, যা আধুনিক AI/ML মডেল ট্রেনিং-এর কেন্দ্রীয় অপারেশন। এই সাধারণ-উদ্দেশ্যে GPU ব্যবহারকেই GPGPU বলা হয় — আজকের AI যুগের একটি বড় হার্ডওয়্যার চালিকাশক্তি।

৪ · GPU কখন সাহায্য করে, কখন করে না

একটি সৎ, গুরুত্বপূর্ণ সত্য: GPU সবকিছুতে CPU-কে হারায় না। GPU দারুণ কাজ করে যখন কাজটি highly parallel এবং data-parallel — একই সরল অপারেশন বিপুল পরিমাণ স্বাধীন ডেটার ওপর প্রয়োগ করতে হয়। কিন্তু জটিল শাখা-নির্ভরতা, sequential dependency, বা low-latency সিঙ্গেল-থ্রেড পারফরম্যান্স দরকার হলে GPU আসলে CPU-এর চেয়ে খারাপ — এই কারণেই বাস্তব সিস্টেমে GPU কখনো CPU-কে সম্পূর্ণ প্রতিস্থাপন করে না, বরং দুটো একসাথে সহাবস্থান করে, প্রতিটি নিজের উপযুক্ত কাজের জন্য অপটিমাইজড।

নিচের কোড সেলে CPU-স্টাইল (এক এক করে) বনাম GPU-স্টাইল (ব্যাচে-ব্যাচে, বহু "লেন" সমান্তরালে) প্রসেসিং সিমুলেট করে দেখা যাক একটি বিশাল, embarrassingly-parallel কাজে বাস্তব সাইকেল-সংখ্যার পার্থক্য কতটা বড়।

Python
import math

def cpu_style_processing(data, per_element_cycles):
    """CPU-স্টাইল -- প্রতিটি এলিমেন্ট একে একে, সিকোয়েনশিয়ালি প্রসেস হয়"""
    total_cycles = 0
    for _ in data:
        total_cycles += per_element_cycles  # প্রতিটি এলিমেন্ট নিজের সময় নেয়
    return total_cycles


def gpu_style_processing(data, per_element_cycles, num_parallel_cores):
    """GPU-স্টাইল -- num_parallel_cores সংখ্যক 'লেন' একসাথে একই কাজ করে
    (প্রকৃত GPU হার্ডওয়্যারে এটি সত্যিকার সমান্তরাল ALU লেন দিয়ে হয় -- এখানে
    শুধু ব্যাচ-গণনার মাধ্যমে ধারণাগতভাবে সিমুলেট করা হচ্ছে)"""
    num_batches = math.ceil(len(data) / num_parallel_cores)
    total_cycles = num_batches * per_element_cycles  # একটি ব্যাচের সব লেন সমান্তরালে শেষ হয়
    return total_cycles


data = list(range(10_000))       # ১০,০০০টি স্বাধীন এলিমেন্ট -- embarrassingly parallel কাজ
per_element_cycles = 5           # প্রতিটি এলিমেন্টের হিসাবে (সিমুলেটেড) খরচ
num_parallel_cores = 1000        # GPU-স্টাইলে একসাথে চলা "লেন" সংখ্যা

cpu_cycles = cpu_style_processing(data, per_element_cycles)
gpu_cycles = gpu_style_processing(data, per_element_cycles, num_parallel_cores)

print(f"এলিমেন্ট সংখ্যা: {len(data):,}")
print(f"CPU-স্টাইল (সিকোয়েনশিয়াল) মোট সাইকেল: {cpu_cycles:,}")
print(f"GPU-স্টাইল ({num_parallel_cores} সমান্তরাল লেন) মোট সাইকেল: {gpu_cycles:,}")
print(f"স্পিডআপ: {cpu_cycles / gpu_cycles:.1f}x")

    
লক্ষ্য করুন এই বিশাল স্পিডআপ শুধুমাত্র সম্ভব হয়েছে কারণ কাজটি embarrassingly parallel — প্রতিটি এলিমেন্ট সম্পূর্ণ স্বাধীন, একে অপরের ওপর নির্ভর করে না। যদি প্রতিটি এলিমেন্টের হিসাব আগের এলিমেন্টের ফলাফলের ওপর নির্ভর করত (sequential dependency), GPU-স্টাইল ব্যাচিং এভাবে কাজ করতে পারত না।
মূল কথা · Key takeaway

GPU হলো L50-এর SIMD ধারণাকে হাজার-গুণ স্কেলে বাস্তবায়ন করা হার্ডওয়্যার — প্রতি-কোর জটিলতা ত্যাগ করে বিপুল সমান্তরাল থ্রুপুট অর্জন করে। এই trade-off-টাই ঠিক করে দেয় GPU কখন CPU-কে হারায় (বিপুল, স্বাধীন, একই ধরনের ডেটা) আর কখন হারায় না (জটিল, শাখা-নির্ভর, sequential কাজ)।

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

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

প্র ০১ GPU প্রথমে গ্রাফিক্স রেন্ডারিং-এর জন্য ডিজাইন হলেও কীভাবে সেটা AI/ML-এর মতো সম্পূর্ণ ভিন্ন কাজে GPGPU হিসেবে ব্যবহৃত হতে শুরু করল?

গ্রাফিক্স রেন্ডারিং-এর মূল প্রয়োজন ছিল "একই সরল গাণিতিক অপারেশন বিপুল, স্বাধীন ডেটার ওপর একসাথে চালানো" — পিক্সেল-বাই-পিক্সেল। প্রকৌশলীরা লক্ষ্য করলেন AI/ML-এর মূল অপারেশন (ম্যাট্রিক্স গুণন, নিউরাল নেটওয়ার্কের ওজন আপডেট) হুবহু একই প্যাটার্নের — বিপুল, স্বাধীন, একই ধরনের গণনা। যেহেতু GPU হার্ডওয়্যার ইতিমধ্যে ঠিক এই প্যাটার্নের জন্যই অপটিমাইজড ছিল, সেটাকে গ্রাফিক্সের নির্দিষ্ট প্রেক্ষাপট থেকে বের করে সাধারণ-উদ্দেশ্যে (general-purpose) ব্যবহারযোগ্য করাটাই ছিল GPGPU-এর মূল ধারণা।

প্র ০২ একটি জটিল if-else শাখাযুক্ত সিকোয়েনশিয়াল অ্যালগরিদম কি GPU-তে ভালো চলবে? কেন না?

না, সাধারণত ভালো চলবে না। GPU-এর হাজার হাজার সরল কোরে ন্যূনতম কন্ট্রোল লজিক ও শাখা-প্রেডিকশন ক্ষমতা থাকে (M7/L35-এর তুলনায় অনেক দুর্বল) — অনেকগুলো লেন একসাথে একই ইনস্ট্রাকশন চালানোর জন্য অপটিমাইজড, আলাদা আলাদা শাখায় বিচ্ছিন্নভাবে চলার জন্য নয়। ভারী শাখা-নির্ভরতা বা sequential dependency থাকলে GPU-এর "সবাই একসাথে একই কাজ করে" মডেলটাই ভেঙে পড়ে, আর তখন CPU-এর সফিস্টিকেটেড, প্রতি-কোর-শক্তিশালী ডিজাইনই ভালো ফল দেয়।

প্র ০৩ কোড সেলে num_parallel_cores বাড়ালে GPU-স্টাইল সাইকেল সংখ্যা কীভাবে পরিবর্তিত হবে?

gpu_style_processing-এ মোট সাইকেল = ceil(len(data) / num_parallel_cores) * per_element_cycles। num_parallel_cores বাড়ালে ব্যাচ-সংখ্যা (num_batches) কমে যাবে, ফলে মোট সাইকেলও কমবে — যতক্ষণ না num_parallel_cores ডেটার আকারের সমান বা তার বেশি হয়ে যায় (তখন সবকিছু একটাই ব্যাচে, মাত্র per_element_cycles সাইকেলেই শেষ)। এটাই দেখায় কেন GPU নির্মাতারা যতটা সম্ভব বেশি সমান্তরাল কোর গাদাগাদি করে বসাতে চেষ্টা করেন।

অনুশীলন

  1. চিন্তা করুন: AI মডেল ট্রেনিং-এ কেন GPU (বা এমনকি আরও specialized AI-accelerator চিপ) এত ব্যাপকভাবে ব্যবহৃত হয়, CPU নয়?

    নিউরাল নেটওয়ার্ক ট্রেনিং-এর মূল কাজ হলো বিপুল পরিমাণ ম্যাট্রিক্স/ভেক্টর গুণন-যোগ — লক্ষ লক্ষ স্বাধীন গাণিতিক অপারেশন যেগুলো একই ধরনের এবং সমান্তরালে করা যায় (এই পাঠের কোড সেলের embarrassingly-parallel প্যাটার্নের ঠিক বাস্তব উদাহরণ)। GPU-এর হাজার হাজার সরল কোর এই কাজে CPU-এর অল্প কিছু জটিল কোরের চেয়ে বহুগুণ বেশি থ্রুপুট দেয় — এই কারণেই আধুনিক AI ট্রেনিং প্রায় সবসময় GPU (বা GPU-অনুপ্রাণিত specialized হার্ডওয়্যার) ব্যবহার করে।

  2. পরীক্ষা করুন: কোড সেলে num_parallel_cores=1 বসালে gpu_style_processing-এর ফলাফল cpu_style_processing-এর সমান হবে কি না চিন্তা করুন (এখনো কোড পরিবর্তন করবেন না)।

    হ্যাঁ, একদম সমান হবে। num_parallel_cores=1 মানে প্রতি ব্যাচে মাত্র ১টি এলিমেন্ট — অর্থাৎ num_batches = len(data), যা ঠিক CPU-স্টাইলের মতোই এক এক করে সিকোয়েনশিয়ালি প্রসেস করার সমতুল্য। এটাই দেখায় CPU-স্টাইল প্রসেসিং আসলে GPU-স্টাইলেরই একটি বিশেষ কেস — যেখানে "সমান্তরাল লেন" সংখ্যা মাত্র ১।

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

আগের পাঠ
ক্যাশ কোহেরেন্স প্রবলেম