পাঠ ৪৯ · ৫৭-এর মধ্যে · মডিউল ১০
Home / Courses / Numerical Methods / ভেক্টরাইজেশন

প্যারালাল ও ভেক্টরাইজড নিউমেরিক্যাল কম্পিউটিং

Parallel & vectorized numerical computing
৯ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • প্যারালাল কম্পিউটিং ও ভেক্টরাইজেশনের মধ্যে পার্থক্য
  • কেন এই কোর্সের Pyodide স্যান্ডবক্স প্রকৃত প্যারালালিজম বা নির্ভরযোগ্য টাইমিং তুলনা দেখাতে পারে না
  • ভেক্টরাইজেশন কনসেপ্টচুয়ালি কী সুবিধা দেয় — Python-লেভেল লুপ ইটারেশন কমানো, প্রতি "অপারেশনে" বেশি কাজ করা
  • একটি সত্যিকারের অপারেশন-কাউন্ট তুলনা যা O(n²) ও O(n³) বৃদ্ধির হার নিজে গণনা করে দেখায়

১ · প্যারালাল বনাম ভেক্টরাইজড কম্পিউটিং — সংজ্ঞাগত পার্থক্য

প্যারালাল কম্পিউটিং মানে একটি সমস্যাকে ছোট ছোট স্বাধীন অংশে ভেঙে একাধিক CPU কোর, GPU থ্রেড, বা এমনকি একাধিক মেশিনে একসাথে চালানো — যেমন একটি বড় ম্যাট্রিক্সের প্রতিটি সারির গণনা আলাদা কোরে পাঠানো। ভেক্টরাইজেশনVectorizationএকটি এলিমেন্ট-বাই-এলিমেন্ট অপারেশনকে (যেমন দুটি লিস্টের প্রতিটি জোড়া যোগ করা) একটি একক উচ্চ-স্তরের কল হিসেবে প্রকাশ করা, যার প্রকৃত লুপিং নিচের স্তরে (সাধারণত কম্পাইল্ড C/Fortran কোডে) ঘটে, পাইথন ইন্টারপ্রেটার লেভেলে নয়। একটু ভিন্ন ধারণা — এখানে মূল বিষয় হলো কীভাবে অপারেশনটি বর্ণনা করা হয়, প্রয়োজনীয় নয় যে এটি একাধিক কোরে সমান্তরালভাবে চলবে (যদিও অনেক ভেক্টরাইজড লাইব্রেরি অন্তর্নিহিতভাবে প্যারালালিজমও ব্যবহার করে, যেমন SIMD ইনস্ট্রাকশন)। NumPy-এর মতো লাইব্রেরিতে a + b (দুটি অ্যারে) লিখলে, পুরো যোগ অপারেশনটি একবারে কম্পাইল্ড কোডে সম্পন্ন হয় — Python ইন্টারপ্রেটারকে প্রতিটি এলিমেন্টের জন্য আলাদা বাইটকোড চালাতে হয় না।

সরলীকরণের ঘোষণা · Scope note

এই কোর্সের প্রতিটি কোড সেল চলে Pyodide-তে, যা ব্রাউজারে চলা একক-থ্রেডেড WebAssembly-ভিত্তিক Python — তাই এই পাঠে প্রকৃত multi-threading বা multiprocessing বাস্তবায়ন করা হয়নি। আরও গুরুত্বপূর্ণভাবে, Pyodide-এর মধ্যে মাপা যেকোনো ওয়াল-ক্লক সময় (time.time() দিয়ে) ব্রাউজার/ডিভাইস/লোডের উপর নির্ভর করে অনির্ভরযোগ্যভাবে ওঠানামা করে — তাই এই পাঠে কোনো "X গুণ দ্রুত" জাতীয় টাইমিং সংখ্যা দাবি করা হয়নি। বরং, নিচের ডেমো সম্পূর্ণভাবে অপারেশন-কাউন্ট (কতগুলো পৃথক স্কেলার গণনা প্রকৃতপক্ষে ঘটছে) দিয়ে ভেক্টরাইজেশনের ধারণাটি ব্যাখ্যা করে।

২ · ভেক্টরাইজেশন কনসেপ্টচুয়ালি কী কিনে দেয়

কম পাইথন-লেভেল লুপ ইটারেশন
একটি হাতে-লেখা for লুপে প্রতিটি এলিমেন্টের জন্য পাইথন ইন্টারপ্রেটারকে আলাদাভাবে বাইটকোড এক্সিকিউট করতে হয় (টাইপ চেক, ফাংশন কল ওভারহেড সহ); একটি ভেক্টরাইজড কল একই কাজ একটি মাত্র হাই-লেভেল "অপারেশন" হিসেবে ইস্যু করে।
প্রতি কলে বেশি কাজ
ভেক্টরাইজড লাইব্রেরি একটি একক কলে সম্পূর্ণ অ্যারে/ম্যাট্রিক্সের উপর অপারেশন সম্পন্ন করার নির্দেশ পায় — নিচের কোড সেলে n×n matvec-এ n²টি পৃথক পাইথন-লেভেল মাল্টিপ্লাই-অ্যাড লাগে, যেখানে একটি ভেক্টরাইজড কল সেটাকে একটিমাত্র হাই-লেভেল কল হিসেবে প্রকাশ করত।
এই কোর্সের সুযোগ ও সীমা
এই কোর্সের সব কোড সেল ইচ্ছাকৃতভাবে হাতে-লেখা, ভেক্টরাইজেশন ছাড়া (CLAUDE.md-এর "no NumPy" নিয়ম অনুযায়ী) — যাতে অ্যালগরিদমগুলো ভেতর থেকে ঠিক কীভাবে কাজ করে তা স্পষ্ট বোঝা যায়; বাস্তব প্রোডাকশন কোডে এই একই অপারেশনগুলো সাধারণত ভেক্টরাইজড লাইব্রেরি দিয়ে করা হয়।

৩ · একটি সত্যিকারের অপারেশন-কাউন্ট ডেমো

নিচের কোড সেলে ম্যাট্রিক্স-ভেক্টর (matvec) ও ম্যাট্রিক্স-ম্যাট্রিক্স (matmul) গুণ হাতে-লেখা নেস্টেড লুপ দিয়ে করা হয়েছে, এবং প্রতিটি স্কেলার মাল্টিপ্লাই-অ্যাড ধাপে একটি কাউন্টার সত্যিই বৃদ্ধি করা হয়েছে (অনুমান নয়) — যাতে দেখানো যায় লুপ-ভিত্তিক পদ্ধতিতে ঠিক কতগুলো পৃথক পাইথন-লেভেল অপারেশন ঘটে, আর n বাড়লে এই সংখ্যা কীভাবে বৃদ্ধি পায় (O(n²) ও O(n³))। একটি ভেক্টরাইজড লাইব্রেরিতে প্রতিটি সারিতে "vectorized call count" কলামের মান সবসময় ১ — পুরো matvec বা matmul একটি মাত্র উচ্চ-স্তরের কল হিসেবে ইস্যু করা হতো।

Python
import random

random.seed(7)

def make_matrix(n):
    return [[round(random.uniform(-3, 3), 2) for _ in range(n)] for _ in range(n)]

def make_vector(n):
    return [round(random.uniform(-3, 3), 2) for _ in range(n)]

def manual_matvec(M, v):
    n = len(M)
    result = [0.0]*n
    py_level_scalar_ops = 0  # ইন্টারপ্রেটার প্রকৃতপক্ষে যতবার multiply-add চালায়
    for i in range(n):
        s = 0.0
        for j in range(n):
            s += M[i][j]*v[j]
            py_level_scalar_ops += 1
        result[i] = s
    return result, py_level_scalar_ops

def manual_matmul(A, B):
    n = len(A)
    result = [[0.0]*n for _ in range(n)]
    py_level_scalar_ops = 0
    for i in range(n):
        for j in range(n):
            s = 0.0
            for k in range(n):
                s += A[i][k]*B[k][j]
                py_level_scalar_ops += 1
            result[i][j] = s
    return result, py_level_scalar_ops

print(f"{'n':>4} | {'matvec: Python-level scalar ops':>32} | {'vectorized call count':>22}")
for n in [4, 8, 16, 32, 64]:
    M = make_matrix(n)
    v = make_vector(n)
    _, ops = manual_matvec(M, v)
    print(f"{n:4d} | {ops:32d} | {1:22d}")

print()
print(f"{'n':>4} | {'matmul: Python-level scalar ops':>32} | {'vectorized call count':>22}")
for n in [4, 8, 16, 32]:
    A = make_matrix(n)
    B = make_matrix(n)
    _, ops = manual_matmul(A, B)
    print(f"{n:4d} | {ops:32d} | {1:22d}")

print()
n1, n2 = 16, 32
M1, v1 = make_matrix(n1), make_vector(n1)
M2, v2 = make_matrix(n2), make_vector(n2)
_, ops1 = manual_matvec(M1, v1)
_, ops2 = manual_matvec(M2, v2)
print(f"matvec অপারেশন n={n1}: {ops1}, n={n2}: {ops2}, অনুপাত: {ops2/ops1:.2f} (O(n^2)-এর জন্য প্রত্যাশিত ~{(n2/n1)**2:.2f})")

A1, B1 = make_matrix(n1), make_matrix(n1)
A2, B2 = make_matrix(n2), make_matrix(n2)
_, mops1 = manual_matmul(A1, B1)
_, mops2 = manual_matmul(A2, B2)
print(f"matmul অপারেশন n={n1}: {mops1}, n={n2}: {mops2}, অনুপাত: {mops2/mops1:.2f} (O(n^3)-এর জন্য প্রত্যাশিত ~{(n2/n1)**3:.2f})")

    
n=16 থেকে n=32-এ (দ্বিগুণ) যেতে matvec-এর অপারেশন-কাউন্ট 256 থেকে 1024-এ যায় — ঠিক ৪.০০ গুণ (2² = 4, matvec-এর O(n²) স্কেলিং-এর সাথে হুবহু মিলে যায়)। matmul-এর অপারেশন-কাউন্ট 4096 থেকে 32768-এ যায় — ঠিক ৮.০০ গুণ (2³ = 8, O(n³) স্কেলিং-এর সাথে হুবহু মিলে যায়)। এই দুটো অনুপাতই কোডের নিজের কাউন্টার থেকে সত্যিই গণনা করে বের করা, কোনো তত্ত্ব থেকে ধরে নেওয়া নয়। একটি ভেক্টরাইজড লাইব্রেরিতে "vectorized call count" কলাম সবসময় থাকত 1 — অপারেশনটি ইস্যু করার দিক থেকে n বাড়লেও পাইথন-লেভেল কলের সংখ্যা বাড়ে না, যদিও নিচের স্তরে (কম্পাইল্ড কোডে) একই পরিমাণ প্রকৃত পাটিগণিতিক কাজ ঘটে।
মূল কথা · Key takeaway

ভেক্টরাইজেশন গণনার মোট পরিমাণ কমায় না (matvec এখনও O(n²), matmul এখনও O(n³) পাটিগণিতিক কাজ — ../design-and-analysis-of-algorithms/index.html-এ এই জটিলতা-শ্রেণীর গভীর আলোচনা) — বরং সেই কাজটি কীভাবে ইস্যু করা হয় তা বদলে দেয়: পাইথন-লেভেল ইন্টারপ্রেটেশন ওভারহেডসহ প্রতিটি ধাপ আলাদা করে না চালিয়ে, একটি একক হাই-লেভেল কলে পুরো কাজ কম্পাইল্ড কোডে পাঠিয়ে দেয় (এবং প্রায়ই অন্তর্নিহিতভাবে SIMD/মাল্টি-কোর প্যারালালিজমও ব্যবহার করে)। এই পার্থক্যটাই বাস্তব প্র্যাকটিসে NumPy-এর মতো লাইব্রেরিগুলোকে হাতে-লেখা পাইথন লুপের চেয়ে উল্লেখযোগ্যভাবে দ্রুত করে তোলে — যদিও এই কোর্স সেই দাবি নিজে পরিমাপ করে না দেখানোর কারণ উপরে বলা হয়েছে।

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

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

প্র ০১ কোড সেলে কেন ওয়াল-ক্লক সময় (time.time()) দিয়ে "কতটা দ্রুত" তুলনা না করে, শুধু অপারেশন-কাউন্ট গোনা হয়েছে?

Pyodide ব্রাউজারে WebAssembly হিসেবে চলে, এবং ওয়াল-ক্লক টাইমিং ডিভাইসের CPU, ব্রাউজার লোড, ব্যাকগ্রাউন্ড ট্যাব থ্রটলিং ও অন্যান্য অনির্দিষ্ট ফ্যাক্টরের উপর নির্ভর করে ব্যাপকভাবে পরিবর্তিত হতে পারে — একই কোড দুইবার চালালেও ভিন্ন সময় দিতে পারে। অপারেশন-কাউন্ট (কতগুলো পৃথক মাল্টিপ্লাই-অ্যাড প্রকৃতপক্ষে ঘটেছে) এর বিপরীতে সম্পূর্ণ নির্ধারিত (deterministic) এবং পুনরুৎপাদনযোগ্য — তাই এটি ভেক্টরাইজেশনের ধারণাটি নির্ভরযোগ্যভাবে ব্যাখ্যা করার জন্য অনেক বেশি উপযুক্ত পরিমাপ।

প্র ০২ matvec-এর অপারেশন-কাউন্ট O(n²) আর matmul-এর O(n³) — এই দুটোর মধ্যে বৃদ্ধির হারের পার্থক্য প্র্যাকটিক্যালি কী বোঝায়, বড় n-এর জন্য?

n দ্বিগুণ করলে matvec-এর কাজ 4 গুণ বাড়ে, কিন্তু matmul-এর কাজ 8 গুণ বাড়ে — কোড সেলে ঠিক এই সংখ্যাগুলোই বাস্তবে দেখা গেছে। বড় n-এর জন্য (যেমন n = 1000), matmul-এর O(n³) স্কেলিং matvec-এর O(n²)-এর চেয়ে বহুগুণ দ্রুত বেড়ে যায় — এই কারণেই বড় ম্যাট্রিক্স-ম্যাট্রিক্স গুণ ভেক্টরাইজড/অপ্টিমাইজড লাইব্রেরি (বা GPU) ছাড়া ব্যবহারিকভাবে অসম্ভব হয়ে পড়ে, যেখানে matvec তুলনামূলক অনেক বেশি সহনীয় থাকে।

প্র ০৩ "vectorized call count সবসময় ১" — এই বাক্যটি কি বোঝায় ভেক্টরাইজেশন প্রকৃত গণনার কাজ কমিয়ে দেয়? না হলে, এটি আসলে কী কমায়?

না — মূল পাটিগণিতিক কাজের পরিমাণ (কতগুলো গুণ ও যোগ প্রয়োজন) একই থাকে, ভেক্টরাইজড হোক বা না হোক। "vectorized call count = 1" শুধু বোঝায় পাইথন ইন্টারপ্রেটার-স্তরে কতগুলো পৃথক নির্দেশ ইস্যু করতে হয়েছে তা কমেছে — আসল লুপিং তখন কম্পাইল্ড, নিম্ন-স্তরের কোডে চলে যায়, যা পাইথন ইন্টারপ্রেটেশন ওভারহেড ছাড়াই দ্রুত চলতে পারে (এবং প্রায়ই সমান্তরালভাবেও)। তাই "কম কাজ" নয়, বরং "একই কাজ কম ওভারহেডে ইস্যু করা" — এটাই ভেক্টরাইজেশনের প্রকৃত সুবিধা।

অনুশীলন

  1. চিন্তা করুন: উপরের কোড সেলে n1, n2 = 16, 32-কে n1, n2 = 8, 32 (৪ গুণ, দ্বিগুণ নয়) করলে matvec অপারেশন-কাউন্টের অনুপাত কত হবে বলে আপনার ধারণা?

    matvec O(n²), তাই n ৪ গুণ বাড়লে অপারেশন-কাউন্ট 4² = 16 গুণ বাড়ার কথা।

  2. পরীক্ষা করুন: উপরের কোড সেলে n1, n2 = 16, 32-কে n1, n2 = 8, 32 তে পরিবর্তন করে Run চেপে matvec অনুপাত সংখ্যা দেখুন এবং আপনার অনুমানের সাথে মিলিয়ে দেখুন।

    n=8-এ matvec অপারেশন 64, n=32-এ 1024 — অনুপাত 1024/64 = 16.00, যা ঠিক (32/8)² = 16-এর সাথে হুবহু মিলে যায়, নিশ্চিত করে matvec-এর O(n²) স্কেলিং কোড সেলের নিজের গণনায় সত্যিই প্রতিফলিত হচ্ছে।

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

আগের পাঠ
স্পার্স ম্যাট্রিক্স টেকনিক ও কম্পিউটেশনাল কমপ্লেক্সিটি