ওয়েবে GPU Compute দিয়ে শুরু করুন

এই পোস্টটি উদাহরণের মাধ্যমে পরীক্ষামূলক WebGPU API অন্বেষণ করে এবং আপনাকে GPU ব্যবহার করে ডেটা-সমান্তরাল গণনা করা শুরু করতে সাহায্য করে।

ফ্রাঁসোয়া বিউফোর্ট
François Beaufort

পটভূমি

আপনি ইতিমধ্যে জানেন যে, গ্রাফিক প্রসেসিং ইউনিট (GPU) হল একটি কম্পিউটারের মধ্যে একটি ইলেকট্রনিক সাবসিস্টেম যা মূলত গ্রাফিক্স প্রক্রিয়াকরণের জন্য বিশেষায়িত ছিল। যাইহোক, বিগত 10 বছরে, এটি একটি আরও নমনীয় আর্কিটেকচারের দিকে বিকশিত হয়েছে যা ডেভেলপারদের GPU-এর অনন্য আর্কিটেকচারের সুবিধা নেওয়ার সময়, শুধুমাত্র 3D গ্রাফিক্স রেন্ডার না করে, অনেক ধরনের অ্যালগরিদম বাস্তবায়ন করতে দেয়। এই ক্ষমতাগুলিকে জিপিইউ কম্পিউট হিসাবে উল্লেখ করা হয় এবং সাধারণ-উদ্দেশ্য বৈজ্ঞানিক কম্পিউটিংয়ের জন্য একটি জিপিইউকে একটি সহপ্রসেসর হিসাবে ব্যবহার করাকে সাধারণ-উদ্দেশ্যের জিপিইউ (GPGPU) প্রোগ্রামিং বলা হয়।

GPU Compute সাম্প্রতিক মেশিন লার্নিং বুমে উল্লেখযোগ্যভাবে অবদান রেখেছে, কারণ কনভোলিউশন নিউরাল নেটওয়ার্ক এবং অন্যান্য মডেলগুলি GPU-তে আরও দক্ষতার সাথে চালানোর জন্য আর্কিটেকচারের সুবিধা নিতে পারে। বর্তমান ওয়েব প্ল্যাটফর্মে GPU কম্পিউট ক্ষমতার অভাব থাকায়, W3C-এর "ওয়েবের জন্য GPU" কমিউনিটি গ্রুপ একটি এপিআই ডিজাইন করছে যাতে আধুনিক GPU এপিআইগুলিকে প্রকাশ করা যায় যা বেশিরভাগ বর্তমান ডিভাইসে উপলব্ধ। এই API কে WebGPU বলা হয়।

WebGPU হল একটি নিম্ন-স্তরের API, যেমন WebGL। এটা খুবই শক্তিশালী এবং বেশ শব্দসমৃদ্ধ, আপনি দেখতে পাবেন। কিন্তু যে ঠিক আছে. আমরা যা খুঁজছি তা হল কর্মক্ষমতা।

এই নিবন্ধে, আমি WebGPU-এর GPU কম্পিউট অংশে ফোকাস করতে যাচ্ছি এবং সত্যি কথা বলতে, আমি শুধু সারফেস স্ক্র্যাচ করছি, যাতে আপনি নিজে থেকে খেলা শুরু করতে পারেন। আমি আরও গভীরে ডুব দেব এবং আসন্ন নিবন্ধগুলিতে WebGPU রেন্ডারিং (ক্যানভাস, টেক্সচার, ইত্যাদি) কভার করব।

GPU অ্যাক্সেস করুন

WebGPU-তে GPU অ্যাক্সেস করা সহজ। navigator.gpu.requestAdapter() কল করা একটি জাভাস্ক্রিপ্ট প্রতিশ্রুতি প্রদান করে যা একটি GPU অ্যাডাপ্টারের সাথে অ্যাসিঙ্ক্রোনাসভাবে সমাধান করবে। এই অ্যাডাপ্টারটিকে গ্রাফিক্স কার্ড হিসাবে ভাবুন। এটি হয় ইন্টিগ্রেটেড হতে পারে (CPU-এর মতো একই চিপে) অথবা বিচ্ছিন্ন (সাধারণত একটি PCIe কার্ড যা বেশি পারফরম্যান্স কিন্তু বেশি শক্তি ব্যবহার করে)।

একবার আপনার কাছে GPU অ্যাডাপ্টার হয়ে গেলে, একটি প্রতিশ্রুতি পেতে adapter.requestDevice() কল করুন যা একটি GPU ডিভাইসের সাথে সমাধান করবে যা আপনি কিছু GPU গণনা করতে ব্যবহার করবেন।

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();

উভয় ফাংশন বিকল্পগুলি গ্রহণ করে যা আপনাকে অ্যাডাপ্টার (পাওয়ার পছন্দ) এবং ডিভাইস (এক্সটেনশন, সীমা) সম্পর্কে নির্দিষ্ট হতে দেয়। সরলতার জন্য, আমরা এই নিবন্ধে ডিফল্ট বিকল্পগুলি ব্যবহার করব।

বাফার মেমরি লিখুন

GPU-এর জন্য মেমরিতে ডেটা লেখার জন্য কীভাবে JavaScript ব্যবহার করবেন তা দেখা যাক। আধুনিক ওয়েব ব্রাউজারে ব্যবহৃত স্যান্ডবক্সিং মডেলের কারণে এই প্রক্রিয়াটি সহজবোধ্য নয়।

নীচের উদাহরণটি আপনাকে দেখায় কিভাবে GPU থেকে অ্যাক্সেসযোগ্য মেমরি বাফার করতে চার বাইট লিখতে হয়। এটি device.createBuffer() কল করে যা বাফারের আকার এবং এর ব্যবহার নেয়। যদিও ব্যবহার পতাকা GPUBufferUsage.MAP_WRITE এই নির্দিষ্ট কলের জন্য প্রয়োজন হয় না, আসুন আমরা এই বাফারে লিখতে চাই। এটির ফলে একটি GPU বাফার অবজেক্ট তৈরির সময় ম্যাপ করা হয় mappedAtCreation সত্যে সেট করা হয়েছে। তারপর GPU বাফার পদ্ধতি getMappedRange() কল করে সংশ্লিষ্ট কাঁচা বাইনারি ডেটা বাফারটি পুনরুদ্ধার করা যেতে পারে।

আপনি ইতিমধ্যে ArrayBuffer এর সাথে খেলে থাকলে বাইট লেখা পরিচিত; একটি TypedArray ব্যবহার করুন এবং এতে মানগুলি অনুলিপি করুন।

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE
});
const arrayBuffer = gpuBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

এই মুহুর্তে, GPU বাফারটি ম্যাপ করা হয়, যার অর্থ এটি CPU এর মালিকানাধীন, এবং এটি JavaScript থেকে রিড/রাইটে অ্যাক্সেসযোগ্য। যাতে GPU এটি অ্যাক্সেস করতে পারে, এটিকে আনম্যাপ করতে হবে যা gpuBuffer.unmap() কল করার মতোই সহজ।

ম্যাপড/আনম্যাপড ধারণাটি রেসের অবস্থা প্রতিরোধ করার জন্য প্রয়োজন যেখানে জিপিইউ এবং সিপিইউ একই সময়ে মেমরি অ্যাক্সেস করে।

বাফার মেমরি পড়ুন

এখন দেখা যাক কিভাবে একটি জিপিইউ বাফারকে অন্য একটি জিপিইউ বাফারে কপি করা যায় এবং তা আবার পড়তে হয়।

যেহেতু আমরা প্রথম GPU বাফারে লিখছি এবং আমরা এটিকে একটি দ্বিতীয় GPU বাফারে অনুলিপি করতে চাই, একটি নতুন ব্যবহারের পতাকা GPUBufferUsage.COPY_SRC প্রয়োজন৷ device.createBuffer() দিয়ে এবার দ্বিতীয় GPU বাফারটি একটি আনম্যাপড অবস্থায় তৈরি করা হয়েছে। এর ব্যবহারের পতাকা হল GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ যেহেতু এটি প্রথম GPU বাফারের গন্তব্য হিসাবে ব্যবহার করা হবে এবং GPU কপি কমান্ডগুলি কার্যকর করা হয়ে গেলে জাভাস্ক্রিপ্টে পড়বে৷

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuWriteBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC
});
const arrayBuffer = gpuWriteBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

// Unmap buffer so that it can be used later for copy.
gpuWriteBuffer.unmap();

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: 4,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

যেহেতু GPU একটি স্বাধীন কপ্রসেসর, সমস্ত GPU কমান্ড অ্যাসিঙ্ক্রোনাসভাবে কার্যকর করা হয়। এই কারণেই সেখানে GPU কমান্ডের একটি তালিকা তৈরি করা হয়েছে এবং প্রয়োজনের সময় ব্যাচে পাঠানো হয়েছে। WebGPU-তে, device.createCommandEncoder() দ্বারা প্রত্যাবর্তিত GPU কমান্ড এনকোডার হল জাভাস্ক্রিপ্ট অবজেক্ট যা "বাফার করা" কমান্ডের একটি ব্যাচ তৈরি করে যা কিছু সময়ে GPU-তে পাঠানো হবে। অন্যদিকে, GPUBuffer এর পদ্ধতিগুলি "আনবাফার" হয়, যার অর্থ তারা যখন বলা হয় তখন পারমাণবিকভাবে চালায়।

একবার আপনার কাছে GPU কমান্ড এনকোডার হয়ে গেলে, পরবর্তী কার্যকর করার জন্য কমান্ড সারিতে এই কমান্ডটি যুক্ত করতে নীচের দেখানো হিসাবে copyEncoder.copyBufferToBuffer() এ কল করুন। অবশেষে, copyEncoder.finish() কল করে এনকোডিং কমান্ড শেষ করুন এবং সেগুলিকে GPU ডিভাইস কমান্ড কিউতে জমা দিন। আর্গুমেন্ট হিসাবে জিপিইউ কমান্ড সহ device.queue.submit() মাধ্যমে জমা দেওয়া সাবমিট পরিচালনার জন্য সারি দায়ী। এটি পরমাণুভাবে অ্যারেতে সংরক্ষিত সমস্ত কমান্ডকে ক্রমানুসারে কার্যকর করবে।

// Encode commands for copying buffer to buffer.
const copyEncoder = device.createCommandEncoder();
copyEncoder.copyBufferToBuffer(
  gpuWriteBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  4 /* size */
);

// Submit copy commands.
const copyCommands = copyEncoder.finish();
device.queue.submit([copyCommands]);

এই মুহুর্তে, GPU সারি কমান্ড পাঠানো হয়েছে, কিন্তু অগত্যা কার্যকর করা হয়নি। দ্বিতীয় GPU বাফার পড়তে, GPUMapMode.READ এর সাথে gpuReadBuffer.mapAsync() কল করুন। এটি একটি প্রতিশ্রুতি প্রদান করে যা GPU বাফার ম্যাপ করা হলে সমাধান করবে। তারপর gpuReadBuffer.getMappedRange() দিয়ে ম্যাপ করা রেঞ্জটি পান যেটিতে প্রথম GPU বাফারের মতো একই মান রয়েছে একবার সমস্ত সারিবদ্ধ GPU কমান্ড কার্যকর করা হয়েছে।

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const copyArrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Uint8Array(copyArrayBuffer));

আপনি এই নমুনা চেষ্টা করতে পারেন.

সংক্ষেপে, বাফার মেমরি অপারেশন সম্পর্কে আপনার যা মনে রাখা দরকার তা এখানে:

  • ডিভাইস সারি জমা দেওয়ার জন্য GPU বাফারগুলিকে আনম্যাপ করতে হবে।
  • ম্যাপ করা হলে, জিপিইউ বাফারগুলি জাভাস্ক্রিপ্টে পড়া এবং লেখা যায়।
  • GPU বাফার ম্যাপ করা হয় যখন mapAsync() এবং createBuffer() mappedAtCreation এর সাথে true সেট করা হয়।

শেডার প্রোগ্রামিং

GPU-তে চলমান প্রোগ্রামগুলি যেগুলি শুধুমাত্র গণনা করে (এবং ত্রিভুজ আঁকে না) তাদের কম্পিউট শেডার বলা হয়। তারা সমান্তরালভাবে শত শত GPU কোর (যা CPU কোরের চেয়ে ছোট) দ্বারা কার্যকর করা হয় যা ডেটা ক্রাঞ্চ করার জন্য একসাথে কাজ করে। তাদের ইনপুট এবং আউটপুট WebGPU-তে বাফার।

WebGPU-তে কম্পিউট শেডারের ব্যবহার ব্যাখ্যা করার জন্য, আমরা ম্যাট্রিক্স গুণের সাথে খেলব, মেশিন লার্নিংয়ের একটি সাধারণ অ্যালগরিদম নীচে চিত্রিত করা হয়েছে।

ম্যাট্রিক্স গুণন চিত্র
ম্যাট্রিক্স গুণন চিত্র

সংক্ষেপে, আমরা যা করতে যাচ্ছি তা এখানে:

  1. তিনটি জিপিইউ বাফার তৈরি করুন (দুটি ম্যাট্রিক্স গুন করার জন্য এবং একটি ফলাফল ম্যাট্রিক্সের জন্য)
  2. কম্পিউট শেডারের জন্য ইনপুট এবং আউটপুট বর্ণনা করুন
  3. কম্পিউট শেডার কোড কম্পাইল করুন
  4. একটি গণনা পাইপলাইন সেট আপ করুন
  5. জিপিইউতে এনকোড করা কমান্ডগুলি ব্যাচে জমা দিন
  6. ফলাফল ম্যাট্রিক্স GPU বাফার পড়ুন

GPU বাফার তৈরি

সরলতার জন্য, ম্যাট্রিক্সগুলিকে ভাসমান বিন্দু সংখ্যার তালিকা হিসাবে উপস্থাপন করা হবে। প্রথম উপাদানটি সারির সংখ্যা, দ্বিতীয় উপাদানটি কলামের সংখ্যা এবং বাকিটি ম্যাট্রিক্সের প্রকৃত সংখ্যা।

জাভাস্ক্রিপ্টে একটি ম্যাট্রিক্সের সহজ উপস্থাপনা এবং গাণিতিক স্বরলিপিতে এর সমতুল্য
জাভাস্ক্রিপ্টে একটি ম্যাট্রিক্সের সহজ উপস্থাপনা এবং গাণিতিক স্বরলিপিতে এর সমতুল্য

তিনটি জিপিইউ বাফার হল স্টোরেজ বাফার কারণ আমাদের কম্পিউট শেডারে ডেটা সংরক্ষণ এবং পুনরুদ্ধার করতে হবে। এটি ব্যাখ্যা করে কেন GPU বাফার ব্যবহারের ফ্ল্যাগগুলিতে GPUBufferUsage.STORAGE অন্তর্ভুক্ত রয়েছে। ফলাফল ম্যাট্রিক্স ব্যবহারের ফ্ল্যাগেও GPUBufferUsage.COPY_SRC রয়েছে কারণ সমস্ত GPU সারি কমান্ডগুলি কার্যকর হয়ে গেলে এটি পড়ার জন্য অন্য বাফারে অনুলিপি করা হবে।

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();


// First Matrix

const firstMatrix = new Float32Array([
  2 /* rows */, 4 /* columns */,
  1, 2, 3, 4,
  5, 6, 7, 8
]);

const gpuBufferFirstMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: firstMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferFirstMatrix = gpuBufferFirstMatrix.getMappedRange();
new Float32Array(arrayBufferFirstMatrix).set(firstMatrix);
gpuBufferFirstMatrix.unmap();


// Second Matrix

const secondMatrix = new Float32Array([
  4 /* rows */, 2 /* columns */,
  1, 2,
  3, 4,
  5, 6,
  7, 8
]);

const gpuBufferSecondMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: secondMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferSecondMatrix = gpuBufferSecondMatrix.getMappedRange();
new Float32Array(arrayBufferSecondMatrix).set(secondMatrix);
gpuBufferSecondMatrix.unmap();


// Result Matrix

const resultMatrixBufferSize = Float32Array.BYTES_PER_ELEMENT * (2 + firstMatrix[0] * secondMatrix[1]);
const resultMatrixBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});

গ্রুপ বিন্যাস এবং আবদ্ধ গ্রুপ আবদ্ধ

বাইন্ড গ্রুপ লেআউট এবং বাইন্ড গ্রুপের ধারণা WebGPU-এর জন্য নির্দিষ্ট। একটি বাইন্ড গ্রুপ লেআউট একটি শেডার দ্বারা প্রত্যাশিত ইনপুট/আউটপুট ইন্টারফেসকে সংজ্ঞায়িত করে, যখন একটি বাইন্ড গ্রুপ একটি শেডারের জন্য প্রকৃত ইনপুট/আউটপুট ডেটা উপস্থাপন করে।

নীচের উদাহরণে, বাইন্ড গ্রুপ লেআউটটি কম্পিউট শেডারের জন্য সংখ্যাযুক্ত এন্ট্রি বাইন্ডিং 0 , 1 , এবং 2 -এ একটি স্টোরেজ বাফার দুটি পঠনযোগ্য স্টোরেজ বাফার আশা করে৷ অন্যদিকে বাইন্ড গ্রুপ, এই বাইন্ড গ্রুপ লেআউটের জন্য সংজ্ঞায়িত, GPU বাফারগুলিকে এন্ট্রিগুলির সাথে যুক্ত করে: বাইন্ডিং 0 এর সাথে gpuBufferFirstMatrix , বাইন্ডিং 1 এর সাথে gpuBufferSecondMatrix , এবং বাইন্ডিং 2 এর সাথে resultMatrixBuffer

const bindGroupLayout = device.createBindGroupLayout({
  entries: [
    {
      binding: 0,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "read-only-storage"
      }
    },
    {
      binding: 1,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "read-only-storage"
      }
    },
    {
      binding: 2,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "storage"
      }
    }
  ]
});

const bindGroup = device.createBindGroup({
  layout: bindGroupLayout,
  entries: [
    {
      binding: 0,
      resource: {
        buffer: gpuBufferFirstMatrix
      }
    },
    {
      binding: 1,
      resource: {
        buffer: gpuBufferSecondMatrix
      }
    },
    {
      binding: 2,
      resource: {
        buffer: resultMatrixBuffer
      }
    }
  ]
});

শেডার কোড গণনা করুন

ম্যাট্রিক্স গুন করার জন্য কম্পিউট শেডার কোডটি WGSL , WebGPU শেডার ল্যাঙ্গুয়েজে লেখা হয়েছে, যা SPIR-V- তে তুচ্ছভাবে অনুবাদযোগ্য। বিশদে না গিয়ে, আপনাকে var<storage> দিয়ে চিহ্নিত তিনটি স্টোরেজ বাফারের নীচে খুঁজে পাওয়া উচিত। প্রোগ্রামটি ইনপুট হিসাবে firstMatrix এবং secondMatrix এবং আউটপুট হিসাবে resultMatrix ব্যবহার করবে।

মনে রাখবেন যে প্রতিটি স্টোরেজ বাফারে একটি binding সজ্জা ব্যবহার করা হয় যা উপরে ঘোষিত বাইন্ড গ্রুপ লেআউট এবং বাইন্ড গ্রুপে সংজ্ঞায়িত একই সূচকের সাথে মিলে যায়।

const shaderModule = device.createShaderModule({
  code: `
    struct Matrix {
      size : vec2f,
      numbers: array<f32>,
    }

    @group(0) @binding(0) var<storage, read> firstMatrix : Matrix;
    @group(0) @binding(1) var<storage, read> secondMatrix : Matrix;
    @group(0) @binding(2) var<storage, read_write> resultMatrix : Matrix;

    @compute @workgroup_size(8, 8)
    fn main(@builtin(global_invocation_id) global_id : vec3u) {
      // Guard against out-of-bounds work group sizes
      if (global_id.x >= u32(firstMatrix.size.x) || global_id.y >= u32(secondMatrix.size.y)) {
        return;
      }

      resultMatrix.size = vec2(firstMatrix.size.x, secondMatrix.size.y);

      let resultCell = vec2(global_id.x, global_id.y);
      var result = 0.0;
      for (var i = 0u; i < u32(firstMatrix.size.y); i = i + 1u) {
        let a = i + resultCell.x * u32(firstMatrix.size.y);
        let b = resultCell.y + i * u32(secondMatrix.size.y);
        result = result + firstMatrix.numbers[a] * secondMatrix.numbers[b];
      }

      let index = resultCell.y + resultCell.x * u32(secondMatrix.size.y);
      resultMatrix.numbers[index] = result;
    }
  `
});

পাইপলাইন সেটআপ

কম্পিউট পাইপলাইন হল সেই বস্তু যা আসলে আমরা যে কম্পিউট অপারেশন করতে যাচ্ছি তা বর্ণনা করে। device.createComputePipeline() কল করে এটি তৈরি করুন। এটির দুটি আর্গুমেন্ট লাগে: আমরা আগে তৈরি করা বাইন্ড গ্রুপ লেআউট, এবং একটি কম্পিউট স্টেজ যা আমাদের কম্পিউট শেডার ( main WGSL ফাংশন) এর এন্ট্রি পয়েন্ট নির্ধারণ করে এবং device.createShaderModule() দিয়ে তৈরি প্রকৃত কম্পিউট শেডার মডিউল।

const computePipeline = device.createComputePipeline({
  layout: device.createPipelineLayout({
    bindGroupLayouts: [bindGroupLayout]
  }),
  compute: {
    module: shaderModule,
    entryPoint: "main"
  }
});

আদেশ জমা

আমাদের তিনটি GPU বাফার এবং একটি বাইন্ড গ্রুপ লেআউট সহ একটি কম্পিউট পাইপলাইন সহ একটি বাইন্ড গ্রুপ ইনস্ট্যান্ট করার পরে, সেগুলি ব্যবহার করার সময় এসেছে৷

commandEncoder.beginComputePass() দিয়ে একটি প্রোগ্রামেবল কম্পিউট পাস এনকোডার শুরু করা যাক। আমরা GPU কমান্ড এনকোড করতে এটি ব্যবহার করব যা ম্যাট্রিক্স গুণন সম্পাদন করবে। passEncoder.setPipeline(computePipeline) এর সাথে এর পাইপলাইন সেট করুন এবং passEncoder.setBindGroup(0, bindGroup) এর সাথে সূচক 0 এ এর ​​বাঁধাই গ্রুপ সেট করুন। সূচক 0 WGSL কোডের group(0) সজ্জার সাথে মিলে যায়।

এখন, এই কম্পিউট শেডারটি GPU-তে কীভাবে চলবে সে সম্পর্কে কথা বলা যাক। আমাদের লক্ষ্য হল ফলাফল ম্যাট্রিক্সের প্রতিটি ঘরের জন্য ধাপে ধাপে সমান্তরালভাবে এই প্রোগ্রামটি কার্যকর করা। উদাহরণস্বরূপ, 16 বাই 32 আকারের ফলাফলের ম্যাট্রিক্সের জন্য, @workgroup_size(8, 8) এ এক্সিকিউশন কমান্ড এনকোড করতে, আমরা passEncoder.dispatchWorkgroups(2, 4) বা passEncoder.dispatchWorkgroups(16 / 8, 32 / 8) কল করব passEncoder.dispatchWorkgroups(16 / 8, 32 / 8) । প্রথম আর্গুমেন্ট "x" হল প্রথম মাত্রা, দ্বিতীয়টি "y" হল দ্বিতীয় মাত্রা, এবং সর্বশেষ একটি "z" হল তৃতীয় মাত্রা যা 1-এ ডিফল্ট হয় কারণ এখানে আমাদের প্রয়োজন নেই৷ জিপিইউ কম্পিউট জগতে, ডেটার একটি সেটে কার্নেল ফাংশন চালানোর জন্য একটি কমান্ড এনকোড করাকে প্রেরণ বলা হয়।

প্রতিটি ফলাফল ম্যাট্রিক্স কক্ষের জন্য সমান্তরালভাবে সম্পাদন
প্রতিটি ফলাফল ম্যাট্রিক্স কক্ষের জন্য সমান্তরালভাবে সম্পাদন

আমাদের কম্পিউট শেডারের জন্য ওয়ার্কগ্রুপ গ্রিডের আকার আমাদের WGSL কোডে (8, 8) । এই কারণে, "x" এবং "y" যেগুলি যথাক্রমে প্রথম ম্যাট্রিক্সের সারি সংখ্যা এবং দ্বিতীয় ম্যাট্রিক্সের কলামের সংখ্যা 8 দ্বারা ভাগ করা হবে। এর সাথে, আমরা এখন passEncoder.dispatchWorkgroups(firstMatrix[0] / 8, secondMatrix[1] / 8) । চালানোর জন্য ওয়ার্কগ্রুপ গ্রিডের সংখ্যা হল dispatchWorkgroups() আর্গুমেন্ট।

উপরের অঙ্কনে দেখা গেছে, প্রতিটি শেডারের একটি অনন্য builtin(global_invocation_id) অবজেক্টে অ্যাক্সেস থাকবে যা কোন ফলাফল ম্যাট্রিক্স সেল গণনা করতে হবে তা জানতে ব্যবহার করা হবে।

const commandEncoder = device.createCommandEncoder();

const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
const workgroupCountX = Math.ceil(firstMatrix[0] / 8);
const workgroupCountY = Math.ceil(secondMatrix[1] / 8);
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY);
passEncoder.end();

কম্পিউট পাস এনকোডার শেষ করতে, passEncoder.end() কল করুন। তারপরে, copyBufferToBuffer এর সাথে ফলাফল ম্যাট্রিক্স বাফার অনুলিপি করতে গন্তব্য হিসাবে ব্যবহার করার জন্য একটি GPU বাফার তৈরি করুন। সবশেষে, copyEncoder.finish() দিয়ে এনকোডিং কমান্ড শেষ করুন এবং GPU কমান্ডের সাথে device.queue.submit() কল করে GPU ডিভাইস সারিতে জমা দিন।

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

// Encode commands for copying buffer to buffer.
commandEncoder.copyBufferToBuffer(
  resultMatrixBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  resultMatrixBufferSize /* size */
);

// Submit GPU commands.
const gpuCommands = commandEncoder.finish();
device.queue.submit([gpuCommands]);

ফলাফল ম্যাট্রিক্স পড়ুন

ফলাফল ম্যাট্রিক্স পড়া GPUMapMode.READ এর সাথে gpuReadBuffer.mapAsync() কল করা এবং সমাধান করার জন্য ফিরে আসার প্রতিশ্রুতির জন্য অপেক্ষা করা যা নির্দেশ করে যে GPU বাফার এখন ম্যাপ করা হয়েছে। এই মুহুর্তে, gpuReadBuffer.getMappedRange() দিয়ে ম্যাপ করা পরিসীমা পাওয়া সম্ভব।

ম্যাট্রিক্স গুণের ফলাফল
ম্যাট্রিক্স গুণের ফলাফল

আমাদের কোডে, DevTools JavaScript কনসোলে লগ ইন করা ফলাফল হল "2, 2, 50, 60, 114, 140"।

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const arrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Float32Array(arrayBuffer));

অভিনন্দন! আপনি এটা তৈরি করেছেন. আপনি নমুনা সঙ্গে খেলতে পারেন.

একটি শেষ কৌশল

আপনার কোড পড়া সহজ করার একটি উপায় হল shader মডিউল থেকে বাইন্ড গ্রুপ লেআউট অনুমান করতে গণনা পাইপলাইনের সহজ getBindGroupLayout পদ্ধতি ব্যবহার করা। এই কৌশলটি একটি কাস্টম বাইন্ড গ্রুপ লেআউট তৈরি করা এবং আপনার কম্পিউট পাইপলাইনে একটি পাইপলাইন লেআউট উল্লেখ করার প্রয়োজনীয়তাকে সরিয়ে দেয় যেমন আপনি নীচে দেখতে পারেন।

পূর্ববর্তী নমুনার জন্য getBindGroupLayout এর একটি চিত্র পাওয়া যায়

 const computePipeline = device.createComputePipeline({
-  layout: device.createPipelineLayout({
-    bindGroupLayouts: [bindGroupLayout]
-  }),
   compute: {
-// Bind group layout and bind group
- const bindGroupLayout = device.createBindGroupLayout({
-   entries: [
-     {
-       binding: 0,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "read-only-storage"
-       }
-     },
-     {
-       binding: 1,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "read-only-storage"
-       }
-     },
-     {
-       binding: 2,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "storage"
-       }
-     }
-   ]
- });
+// Bind group
  const bindGroup = device.createBindGroup({
-  layout: bindGroupLayout,
+  layout: computePipeline.getBindGroupLayout(0 /* index */),
   entries: [

কর্মক্ষমতা ফলাফল

তাহলে কিভাবে একটি GPU তে চলমান ম্যাট্রিক্স গুণন একটি CPU তে চালানোর সাথে তুলনা করে? খুঁজে বের করার জন্য, আমি শুধু একটি CPU-র জন্য বর্ণিত প্রোগ্রামটি লিখেছি। এবং আপনি নীচের গ্রাফে দেখতে পাচ্ছেন, যখন ম্যাট্রিক্সের আকার 256 দ্বারা 256-এর বেশি হয় তখন GPU-এর সম্পূর্ণ শক্তি ব্যবহার করা একটি সুস্পষ্ট পছন্দ বলে মনে হয়।

GPU বনাম CPU বেঞ্চমার্ক
GPU বনাম CPU বেঞ্চমার্ক

এই নিবন্ধটি WebGPU অন্বেষণের আমার যাত্রার শুরু মাত্র। GPU কম্পিউটে এবং রেন্ডারিং (ক্যানভাস, টেক্সচার, স্যাম্পলার) WebGPU-তে কীভাবে কাজ করে সে সম্পর্কে শীঘ্রই আরও বেশি নিবন্ধের প্রত্যাশা করুন।

,

এই পোস্টটি উদাহরণের মাধ্যমে পরীক্ষামূলক WebGPU API অন্বেষণ করে এবং আপনাকে GPU ব্যবহার করে ডেটা-সমান্তরাল গণনা করা শুরু করতে সাহায্য করে।

ফ্রাঁসোয়া বিউফোর্ট
François Beaufort

পটভূমি

আপনি ইতিমধ্যে জানেন যে, গ্রাফিক প্রসেসিং ইউনিট (GPU) হল একটি কম্পিউটারের মধ্যে একটি ইলেকট্রনিক সাবসিস্টেম যা মূলত গ্রাফিক্স প্রক্রিয়াকরণের জন্য বিশেষায়িত ছিল। যাইহোক, বিগত 10 বছরে, এটি একটি আরও নমনীয় আর্কিটেকচারের দিকে বিকশিত হয়েছে যা ডেভেলপারদের GPU-এর অনন্য আর্কিটেকচারের সুবিধা নেওয়ার সময়, শুধুমাত্র 3D গ্রাফিক্স রেন্ডার না করে, অনেক ধরনের অ্যালগরিদম বাস্তবায়ন করতে দেয়। এই ক্ষমতাগুলিকে জিপিইউ কম্পিউট হিসাবে উল্লেখ করা হয় এবং সাধারণ-উদ্দেশ্য বৈজ্ঞানিক কম্পিউটিংয়ের জন্য একটি জিপিইউকে একটি সহপ্রসেসর হিসাবে ব্যবহার করাকে সাধারণ-উদ্দেশ্যের জিপিইউ (GPGPU) প্রোগ্রামিং বলা হয়।

GPU Compute সাম্প্রতিক মেশিন লার্নিং বুমে উল্লেখযোগ্যভাবে অবদান রেখেছে, কারণ কনভোলিউশন নিউরাল নেটওয়ার্ক এবং অন্যান্য মডেলগুলি GPU-তে আরও দক্ষতার সাথে চালানোর জন্য আর্কিটেকচারের সুবিধা নিতে পারে। বর্তমান ওয়েব প্ল্যাটফর্মে GPU কম্পিউট ক্ষমতার অভাব থাকায়, W3C-এর "ওয়েবের জন্য GPU" কমিউনিটি গ্রুপ একটি এপিআই ডিজাইন করছে যাতে আধুনিক GPU এপিআইগুলিকে প্রকাশ করা যায় যা বেশিরভাগ বর্তমান ডিভাইসে উপলব্ধ। এই API কে WebGPU বলা হয়।

WebGPU হল একটি নিম্ন-স্তরের API, যেমন WebGL। এটা খুবই শক্তিশালী এবং বেশ শব্দসমৃদ্ধ, আপনি দেখতে পাবেন। কিন্তু যে ঠিক আছে. আমরা যা খুঁজছি তা হল কর্মক্ষমতা।

এই নিবন্ধে, আমি WebGPU-এর GPU কম্পিউট অংশে ফোকাস করতে যাচ্ছি এবং সত্যি কথা বলতে, আমি শুধু সারফেস স্ক্র্যাচ করছি, যাতে আপনি নিজে থেকে খেলা শুরু করতে পারেন। আমি আরও গভীরে ডুব দেব এবং আসন্ন নিবন্ধগুলিতে WebGPU রেন্ডারিং (ক্যানভাস, টেক্সচার, ইত্যাদি) কভার করব।

GPU অ্যাক্সেস করুন

WebGPU-তে GPU অ্যাক্সেস করা সহজ। navigator.gpu.requestAdapter() কল করা একটি জাভাস্ক্রিপ্ট প্রতিশ্রুতি প্রদান করে যা একটি GPU অ্যাডাপ্টারের সাথে অ্যাসিঙ্ক্রোনাসভাবে সমাধান করবে। এই অ্যাডাপ্টারটিকে গ্রাফিক্স কার্ড হিসাবে ভাবুন। এটি হয় ইন্টিগ্রেটেড হতে পারে (CPU-এর মতো একই চিপে) অথবা বিচ্ছিন্ন (সাধারণত একটি PCIe কার্ড যা বেশি পারফরম্যান্স কিন্তু বেশি শক্তি ব্যবহার করে)।

একবার আপনার কাছে GPU অ্যাডাপ্টার হয়ে গেলে, একটি প্রতিশ্রুতি পেতে adapter.requestDevice() কল করুন যা একটি GPU ডিভাইসের সাথে সমাধান করবে যা আপনি কিছু GPU গণনা করতে ব্যবহার করবেন।

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();

উভয় ফাংশন বিকল্পগুলি গ্রহণ করে যা আপনাকে অ্যাডাপ্টার (পাওয়ার পছন্দ) এবং ডিভাইস (এক্সটেনশন, সীমা) সম্পর্কে নির্দিষ্ট হতে দেয়। সরলতার জন্য, আমরা এই নিবন্ধে ডিফল্ট বিকল্পগুলি ব্যবহার করব।

বাফার মেমরি লিখুন

GPU-এর জন্য মেমরিতে ডেটা লেখার জন্য কীভাবে JavaScript ব্যবহার করবেন তা দেখা যাক। আধুনিক ওয়েব ব্রাউজারে ব্যবহৃত স্যান্ডবক্সিং মডেলের কারণে এই প্রক্রিয়াটি সহজবোধ্য নয়।

নীচের উদাহরণটি আপনাকে দেখায় কিভাবে GPU থেকে অ্যাক্সেসযোগ্য মেমরি বাফার করতে চার বাইট লিখতে হয়। এটি device.createBuffer() কল করে যা বাফারের আকার এবং এর ব্যবহার নেয়। যদিও ব্যবহার পতাকা GPUBufferUsage.MAP_WRITE এই নির্দিষ্ট কলের জন্য প্রয়োজন হয় না, আসুন আমরা এই বাফারে লিখতে চাই। এটির ফলে একটি GPU বাফার অবজেক্ট তৈরির সময় ম্যাপ করা হয় mappedAtCreation সত্যে সেট করা হয়েছে। তারপর GPU বাফার পদ্ধতি getMappedRange() কল করে সংশ্লিষ্ট কাঁচা বাইনারি ডেটা বাফারটি পুনরুদ্ধার করা যেতে পারে।

আপনি ইতিমধ্যে ArrayBuffer এর সাথে খেলে থাকলে বাইট লেখা পরিচিত; একটি TypedArray ব্যবহার করুন এবং এতে মানগুলি অনুলিপি করুন।

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE
});
const arrayBuffer = gpuBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

এই মুহুর্তে, GPU বাফারটি ম্যাপ করা হয়, যার অর্থ এটি CPU এর মালিকানাধীন, এবং এটি JavaScript থেকে রিড/রাইটে অ্যাক্সেসযোগ্য। যাতে GPU এটি অ্যাক্সেস করতে পারে, এটিকে আনম্যাপ করতে হবে যা gpuBuffer.unmap() কল করার মতোই সহজ।

ম্যাপড/আনম্যাপড ধারণাটি রেসের অবস্থা প্রতিরোধ করার জন্য প্রয়োজন যেখানে জিপিইউ এবং সিপিইউ একই সময়ে মেমরি অ্যাক্সেস করে।

বাফার মেমরি পড়ুন

এখন দেখা যাক কিভাবে একটি জিপিইউ বাফারকে অন্য একটি জিপিইউ বাফারে কপি করা যায় এবং তা আবার পড়তে হয়।

যেহেতু আমরা প্রথম GPU বাফারে লিখছি এবং আমরা এটিকে একটি দ্বিতীয় GPU বাফারে অনুলিপি করতে চাই, একটি নতুন ব্যবহারের পতাকা GPUBufferUsage.COPY_SRC প্রয়োজন৷ device.createBuffer() দিয়ে এবার দ্বিতীয় GPU বাফারটি একটি আনম্যাপড অবস্থায় তৈরি করা হয়েছে। এর ব্যবহারের পতাকা হল GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ যেহেতু এটি প্রথম GPU বাফারের গন্তব্য হিসাবে ব্যবহার করা হবে এবং GPU কপি কমান্ডগুলি কার্যকর করা হয়ে গেলে জাভাস্ক্রিপ্টে পড়বে৷

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuWriteBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC
});
const arrayBuffer = gpuWriteBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

// Unmap buffer so that it can be used later for copy.
gpuWriteBuffer.unmap();

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: 4,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

যেহেতু GPU একটি স্বাধীন কপ্রসেসর, সমস্ত GPU কমান্ড অ্যাসিঙ্ক্রোনাসভাবে কার্যকর করা হয়। এই কারণেই সেখানে GPU কমান্ডের একটি তালিকা তৈরি করা হয়েছে এবং প্রয়োজনের সময় ব্যাচে পাঠানো হয়েছে। WebGPU-তে, device.createCommandEncoder() দ্বারা প্রত্যাবর্তিত GPU কমান্ড এনকোডার হল জাভাস্ক্রিপ্ট অবজেক্ট যা "বাফার করা" কমান্ডের একটি ব্যাচ তৈরি করে যা কিছু সময়ে GPU-তে পাঠানো হবে। অন্যদিকে, GPUBuffer এর পদ্ধতিগুলি "আনবাফার" হয়, যার অর্থ তারা যখন বলা হয় তখন পারমাণবিকভাবে চালায়।

একবার আপনার কাছে GPU কমান্ড এনকোডার হয়ে গেলে, পরবর্তী কার্যকর করার জন্য কমান্ড সারিতে এই কমান্ডটি যুক্ত করতে নীচের দেখানো হিসাবে copyEncoder.copyBufferToBuffer() এ কল করুন। অবশেষে, copyEncoder.finish() কল করে এনকোডিং কমান্ড শেষ করুন এবং সেগুলিকে GPU ডিভাইস কমান্ড কিউতে জমা দিন। আর্গুমেন্ট হিসাবে জিপিইউ কমান্ড সহ device.queue.submit() মাধ্যমে জমা দেওয়া সাবমিট পরিচালনার জন্য সারি দায়ী। এটি পরমাণুভাবে অ্যারেতে সংরক্ষিত সমস্ত কমান্ডকে ক্রমানুসারে কার্যকর করবে।

// Encode commands for copying buffer to buffer.
const copyEncoder = device.createCommandEncoder();
copyEncoder.copyBufferToBuffer(
  gpuWriteBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  4 /* size */
);

// Submit copy commands.
const copyCommands = copyEncoder.finish();
device.queue.submit([copyCommands]);

এই মুহুর্তে, GPU সারি কমান্ড পাঠানো হয়েছে, কিন্তু অগত্যা কার্যকর করা হয়নি। দ্বিতীয় GPU বাফার পড়তে, GPUMapMode.READ এর সাথে gpuReadBuffer.mapAsync() কল করুন। এটি একটি প্রতিশ্রুতি প্রদান করে যা GPU বাফার ম্যাপ করা হলে সমাধান করবে। তারপর gpuReadBuffer.getMappedRange() দিয়ে ম্যাপ করা রেঞ্জটি পান যেটিতে প্রথম GPU বাফারের মতো একই মান রয়েছে একবার সমস্ত সারিবদ্ধ GPU কমান্ড কার্যকর করা হয়েছে।

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const copyArrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Uint8Array(copyArrayBuffer));

আপনি এই নমুনা চেষ্টা করতে পারেন.

সংক্ষেপে, বাফার মেমরি অপারেশন সম্পর্কে আপনার যা মনে রাখা দরকার তা এখানে:

  • ডিভাইস সারি জমাতে ব্যবহার করার জন্য GPU বাফারগুলিকে আনম্যাপ করতে হবে।
  • ম্যাপ করা হলে, জিপিইউ বাফারগুলি জাভাস্ক্রিপ্টে পড়া এবং লেখা যায়।
  • GPU বাফার ম্যাপ করা হয় যখন mapAsync() এবং createBuffer() mappedAtCreation এর সাথে true সেট করা হয়।

শেডার প্রোগ্রামিং

GPU-তে চলমান প্রোগ্রামগুলি যেগুলি শুধুমাত্র গণনা করে (এবং ত্রিভুজ আঁকে না) তাদের কম্পিউট শেডার বলা হয়। তারা সমান্তরালভাবে শত শত GPU কোর (যা CPU কোরের চেয়ে ছোট) দ্বারা কার্যকর করা হয় যা ডেটা ক্রাঞ্চ করার জন্য একসাথে কাজ করে। তাদের ইনপুট এবং আউটপুট WebGPU-তে বাফার।

WebGPU-তে কম্পিউট শেডারের ব্যবহার ব্যাখ্যা করার জন্য, আমরা ম্যাট্রিক্স গুণের সাথে খেলব, মেশিন লার্নিংয়ের একটি সাধারণ অ্যালগরিদম নীচে চিত্রিত করা হয়েছে।

ম্যাট্রিক্স গুণন চিত্র
ম্যাট্রিক্স গুণন চিত্র

সংক্ষেপে, আমরা যা করতে যাচ্ছি তা এখানে:

  1. তিনটি জিপিইউ বাফার তৈরি করুন (দুটি ম্যাট্রিক্স গুন করার জন্য এবং একটি ফলাফল ম্যাট্রিক্সের জন্য)
  2. কম্পিউট শেডারের জন্য ইনপুট এবং আউটপুট বর্ণনা করুন
  3. কম্পিউট শেডার কোড কম্পাইল করুন
  4. একটি গণনা পাইপলাইন সেট আপ করুন
  5. জিপিইউতে এনকোড করা কমান্ডগুলি ব্যাচে জমা দিন
  6. ফলাফল ম্যাট্রিক্স GPU বাফার পড়ুন

GPU বাফার তৈরি

সরলতার জন্য, ম্যাট্রিক্সগুলিকে ভাসমান বিন্দু সংখ্যার তালিকা হিসাবে উপস্থাপন করা হবে। প্রথম উপাদানটি সারির সংখ্যা, দ্বিতীয় উপাদানটি কলামের সংখ্যা এবং বাকিটি ম্যাট্রিক্সের প্রকৃত সংখ্যা।

জাভাস্ক্রিপ্টে একটি ম্যাট্রিক্সের সহজ উপস্থাপনা এবং গাণিতিক স্বরলিপিতে এর সমতুল্য
জাভাস্ক্রিপ্টে একটি ম্যাট্রিক্সের সহজ উপস্থাপনা এবং গাণিতিক স্বরলিপিতে এর সমতুল্য

তিনটি জিপিইউ বাফার হল স্টোরেজ বাফার কারণ আমাদের কম্পিউট শেডারে ডেটা সংরক্ষণ এবং পুনরুদ্ধার করতে হবে। এটি ব্যাখ্যা করে কেন GPU বাফার ব্যবহারের ফ্ল্যাগগুলিতে GPUBufferUsage.STORAGE অন্তর্ভুক্ত রয়েছে। ফলাফল ম্যাট্রিক্স ব্যবহারের ফ্ল্যাগেও GPUBufferUsage.COPY_SRC রয়েছে কারণ সমস্ত GPU সারি কমান্ডগুলি কার্যকর হয়ে গেলে এটি পড়ার জন্য অন্য বাফারে অনুলিপি করা হবে।

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();


// First Matrix

const firstMatrix = new Float32Array([
  2 /* rows */, 4 /* columns */,
  1, 2, 3, 4,
  5, 6, 7, 8
]);

const gpuBufferFirstMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: firstMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferFirstMatrix = gpuBufferFirstMatrix.getMappedRange();
new Float32Array(arrayBufferFirstMatrix).set(firstMatrix);
gpuBufferFirstMatrix.unmap();


// Second Matrix

const secondMatrix = new Float32Array([
  4 /* rows */, 2 /* columns */,
  1, 2,
  3, 4,
  5, 6,
  7, 8
]);

const gpuBufferSecondMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: secondMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferSecondMatrix = gpuBufferSecondMatrix.getMappedRange();
new Float32Array(arrayBufferSecondMatrix).set(secondMatrix);
gpuBufferSecondMatrix.unmap();


// Result Matrix

const resultMatrixBufferSize = Float32Array.BYTES_PER_ELEMENT * (2 + firstMatrix[0] * secondMatrix[1]);
const resultMatrixBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});

গ্রুপ বিন্যাস এবং আবদ্ধ গ্রুপ আবদ্ধ

বাইন্ড গ্রুপ লেআউট এবং বাইন্ড গ্রুপের ধারণা WebGPU-এর জন্য নির্দিষ্ট। একটি বাইন্ড গ্রুপ লেআউট একটি শেডার দ্বারা প্রত্যাশিত ইনপুট/আউটপুট ইন্টারফেসকে সংজ্ঞায়িত করে, যখন একটি বাইন্ড গ্রুপ একটি শেডারের জন্য প্রকৃত ইনপুট/আউটপুট ডেটা উপস্থাপন করে।

নীচের উদাহরণে, বাইন্ড গ্রুপ লেআউটটি কম্পিউট শেডারের জন্য সংখ্যাযুক্ত এন্ট্রি বাইন্ডিং 0 , 1 , এবং 2 -এ একটি স্টোরেজ বাফার দুটি পঠনযোগ্য স্টোরেজ বাফার আশা করে৷ অন্যদিকে বাইন্ড গ্রুপ, এই বাইন্ড গ্রুপ লেআউটের জন্য সংজ্ঞায়িত, GPU বাফারগুলিকে এন্ট্রিগুলির সাথে যুক্ত করে: বাইন্ডিং 0 এর সাথে gpuBufferFirstMatrix , বাইন্ডিং 1 এর সাথে gpuBufferSecondMatrix , এবং বাইন্ডিং 2 এর সাথে resultMatrixBuffer

const bindGroupLayout = device.createBindGroupLayout({
  entries: [
    {
      binding: 0,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "read-only-storage"
      }
    },
    {
      binding: 1,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "read-only-storage"
      }
    },
    {
      binding: 2,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "storage"
      }
    }
  ]
});

const bindGroup = device.createBindGroup({
  layout: bindGroupLayout,
  entries: [
    {
      binding: 0,
      resource: {
        buffer: gpuBufferFirstMatrix
      }
    },
    {
      binding: 1,
      resource: {
        buffer: gpuBufferSecondMatrix
      }
    },
    {
      binding: 2,
      resource: {
        buffer: resultMatrixBuffer
      }
    }
  ]
});

শেডার কোড গণনা করুন

ম্যাট্রিক্স গুন করার জন্য কম্পিউট শেডার কোডটি WGSL , WebGPU শেডার ল্যাঙ্গুয়েজে লেখা হয়েছে, যা SPIR-V- তে তুচ্ছভাবে অনুবাদযোগ্য। বিশদে না গিয়ে, আপনাকে var<storage> দিয়ে চিহ্নিত তিনটি স্টোরেজ বাফারের নীচে খুঁজে পাওয়া উচিত। প্রোগ্রামটি ইনপুট হিসাবে firstMatrix এবং secondMatrix এবং resultMatrix এর আউটপুট হিসাবে ব্যবহার করবে।

মনে রাখবেন যে প্রতিটি স্টোরেজ বাফারে একটি binding সজ্জা ব্যবহার করা হয় যা উপরে ঘোষিত বাইন্ড গ্রুপ লেআউট এবং বাইন্ড গ্রুপে সংজ্ঞায়িত একই সূচকের সাথে মিলে যায়।

const shaderModule = device.createShaderModule({
  code: `
    struct Matrix {
      size : vec2f,
      numbers: array<f32>,
    }

    @group(0) @binding(0) var<storage, read> firstMatrix : Matrix;
    @group(0) @binding(1) var<storage, read> secondMatrix : Matrix;
    @group(0) @binding(2) var<storage, read_write> resultMatrix : Matrix;

    @compute @workgroup_size(8, 8)
    fn main(@builtin(global_invocation_id) global_id : vec3u) {
      // Guard against out-of-bounds work group sizes
      if (global_id.x >= u32(firstMatrix.size.x) || global_id.y >= u32(secondMatrix.size.y)) {
        return;
      }

      resultMatrix.size = vec2(firstMatrix.size.x, secondMatrix.size.y);

      let resultCell = vec2(global_id.x, global_id.y);
      var result = 0.0;
      for (var i = 0u; i < u32(firstMatrix.size.y); i = i + 1u) {
        let a = i + resultCell.x * u32(firstMatrix.size.y);
        let b = resultCell.y + i * u32(secondMatrix.size.y);
        result = result + firstMatrix.numbers[a] * secondMatrix.numbers[b];
      }

      let index = resultCell.y + resultCell.x * u32(secondMatrix.size.y);
      resultMatrix.numbers[index] = result;
    }
  `
});

পাইপলাইন সেটআপ

কম্পিউট পাইপলাইন হল সেই বস্তু যা আসলে আমরা যে কম্পিউট অপারেশন করতে যাচ্ছি তা বর্ণনা করে। device.createComputePipeline() কল করে এটি তৈরি করুন। এটির দুটি আর্গুমেন্ট লাগে: আমরা আগে তৈরি করা বাইন্ড গ্রুপ লেআউট, এবং একটি কম্পিউট স্টেজ যা আমাদের কম্পিউট শেডার ( main WGSL ফাংশন) এর এন্ট্রি পয়েন্ট নির্ধারণ করে এবং device.createShaderModule() দিয়ে তৈরি প্রকৃত কম্পিউট শেডার মডিউল।

const computePipeline = device.createComputePipeline({
  layout: device.createPipelineLayout({
    bindGroupLayouts: [bindGroupLayout]
  }),
  compute: {
    module: shaderModule,
    entryPoint: "main"
  }
});

আদেশ জমা

আমাদের তিনটি GPU বাফার এবং একটি বাইন্ড গ্রুপ লেআউট সহ একটি কম্পিউট পাইপলাইন সহ একটি বাইন্ড গ্রুপ ইনস্ট্যান্ট করার পরে, সেগুলি ব্যবহার করার সময় এসেছে৷

commandEncoder.beginComputePass() দিয়ে একটি প্রোগ্রামেবল কম্পিউট পাস এনকোডার শুরু করা যাক। আমরা GPU কমান্ড এনকোড করতে এটি ব্যবহার করব যা ম্যাট্রিক্স গুণন সম্পাদন করবে। passEncoder.setPipeline(computePipeline) এর সাথে এর পাইপলাইন সেট করুন এবং passEncoder.setBindGroup(0, bindGroup) এর সাথে সূচক 0 এ এর ​​বাঁধাই গ্রুপ সেট করুন। সূচক 0 WGSL কোডের group(0) সজ্জার সাথে মিলে যায়।

এখন, এই কম্পিউট শেডারটি GPU-তে কীভাবে চলবে সে সম্পর্কে কথা বলা যাক। আমাদের লক্ষ্য হল ফলাফল ম্যাট্রিক্সের প্রতিটি ঘরের জন্য ধাপে ধাপে সমান্তরালভাবে এই প্রোগ্রামটি কার্যকর করা। উদাহরণস্বরূপ, 16 বাই 32 আকারের ফলাফলের ম্যাট্রিক্সের জন্য, @workgroup_size(8, 8) এ এক্সিকিউশন কমান্ড এনকোড করতে, আমরা passEncoder.dispatchWorkgroups(2, 4) বা passEncoder.dispatchWorkgroups(16 / 8, 32 / 8) কল করব passEncoder.dispatchWorkgroups(16 / 8, 32 / 8) । প্রথম আর্গুমেন্ট "x" হল প্রথম মাত্রা, দ্বিতীয়টি "y" হল দ্বিতীয় মাত্রা, এবং সর্বশেষ একটি "z" হল তৃতীয় মাত্রা যা 1-এ ডিফল্ট হয় কারণ এখানে আমাদের প্রয়োজন নেই৷ জিপিইউ কম্পিউট জগতে, ডেটার একটি সেটে কার্নেল ফাংশন চালানোর জন্য একটি কমান্ড এনকোড করাকে প্রেরণ বলা হয়।

প্রতিটি ফলাফল ম্যাট্রিক্স কক্ষের জন্য সমান্তরালভাবে সম্পাদন
প্রতিটি ফলাফল ম্যাট্রিক্স কক্ষের জন্য সমান্তরালভাবে সম্পাদন

আমাদের কম্পিউট শেডারের জন্য ওয়ার্কগ্রুপ গ্রিডের আকার আমাদের WGSL কোডে (8, 8) । এই কারণে, "x" এবং "y" যেগুলি যথাক্রমে প্রথম ম্যাট্রিক্সের সারি সংখ্যা এবং দ্বিতীয় ম্যাট্রিক্সের কলামের সংখ্যা 8 দ্বারা ভাগ করা হবে। এর সাথে, আমরা এখন passEncoder.dispatchWorkgroups(firstMatrix[0] / 8, secondMatrix[1] / 8) । চালানোর জন্য ওয়ার্কগ্রুপ গ্রিডের সংখ্যা হল dispatchWorkgroups() আর্গুমেন্ট।

উপরের অঙ্কনে দেখা গেছে, প্রতিটি শেডারের একটি অনন্য builtin(global_invocation_id) অবজেক্টে অ্যাক্সেস থাকবে যা কোন ফলাফল ম্যাট্রিক্স সেল গণনা করতে হবে তা জানতে ব্যবহার করা হবে।

const commandEncoder = device.createCommandEncoder();

const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
const workgroupCountX = Math.ceil(firstMatrix[0] / 8);
const workgroupCountY = Math.ceil(secondMatrix[1] / 8);
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY);
passEncoder.end();

কম্পিউট পাস এনকোডার শেষ করতে, passEncoder.end() কল করুন। তারপরে, copyBufferToBuffer এর সাথে ফলাফল ম্যাট্রিক্স বাফার অনুলিপি করতে গন্তব্য হিসাবে ব্যবহার করার জন্য একটি GPU বাফার তৈরি করুন। সবশেষে, copyEncoder.finish() দিয়ে এনকোডিং কমান্ড শেষ করুন এবং GPU কমান্ডের সাথে device.queue.submit() কল করে GPU ডিভাইস সারিতে জমা দিন।

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

// Encode commands for copying buffer to buffer.
commandEncoder.copyBufferToBuffer(
  resultMatrixBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  resultMatrixBufferSize /* size */
);

// Submit GPU commands.
const gpuCommands = commandEncoder.finish();
device.queue.submit([gpuCommands]);

ফলাফল ম্যাট্রিক্স পড়ুন

ফলাফল ম্যাট্রিক্স পড়া GPUMapMode.READ এর সাথে gpuReadBuffer.mapAsync() কল করা এবং সমাধান করার জন্য ফিরে আসার প্রতিশ্রুতির জন্য অপেক্ষা করা যা নির্দেশ করে যে GPU বাফার এখন ম্যাপ করা হয়েছে। এই মুহুর্তে, gpuReadBuffer.getMappedRange() দিয়ে ম্যাপ করা পরিসীমা পাওয়া সম্ভব।

ম্যাট্রিক্স গুণের ফলাফল
ম্যাট্রিক্স গুণের ফলাফল

আমাদের কোডে, DevTools JavaScript কনসোলে লগ ইন করা ফলাফল হল "2, 2, 50, 60, 114, 140"।

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const arrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Float32Array(arrayBuffer));

অভিনন্দন! আপনি এটা তৈরি করেছেন. আপনি নমুনা সঙ্গে খেলতে পারেন.

একটি শেষ কৌশল

আপনার কোড পড়া সহজ করার একটি উপায় হল shader মডিউল থেকে বাইন্ড গ্রুপ লেআউট অনুমান করতে গণনা পাইপলাইনের সহজ getBindGroupLayout পদ্ধতি ব্যবহার করা। এই কৌশলটি একটি কাস্টম বাইন্ড গ্রুপ লেআউট তৈরি করা এবং আপনার কম্পিউট পাইপলাইনে একটি পাইপলাইন লেআউট উল্লেখ করার প্রয়োজনীয়তাকে সরিয়ে দেয় যেমন আপনি নীচে দেখতে পারেন।

পূর্ববর্তী নমুনার জন্য getBindGroupLayout এর একটি চিত্র পাওয়া যায়

 const computePipeline = device.createComputePipeline({
-  layout: device.createPipelineLayout({
-    bindGroupLayouts: [bindGroupLayout]
-  }),
   compute: {
-// Bind group layout and bind group
- const bindGroupLayout = device.createBindGroupLayout({
-   entries: [
-     {
-       binding: 0,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "read-only-storage"
-       }
-     },
-     {
-       binding: 1,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "read-only-storage"
-       }
-     },
-     {
-       binding: 2,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "storage"
-       }
-     }
-   ]
- });
+// Bind group
  const bindGroup = device.createBindGroup({
-  layout: bindGroupLayout,
+  layout: computePipeline.getBindGroupLayout(0 /* index */),
   entries: [

কর্মক্ষমতা ফলাফল

তাহলে কিভাবে একটি GPU তে চলমান ম্যাট্রিক্স গুণন একটি CPU তে চালানোর সাথে তুলনা করে? খুঁজে বের করার জন্য, আমি শুধু একটি CPU-র জন্য বর্ণিত প্রোগ্রামটি লিখেছি। এবং আপনি নীচের গ্রাফে দেখতে পাচ্ছেন, যখন ম্যাট্রিক্সের আকার 256 দ্বারা 256-এর বেশি হয় তখন GPU-এর সম্পূর্ণ শক্তি ব্যবহার করা একটি সুস্পষ্ট পছন্দ বলে মনে হয়।

GPU বনাম CPU বেঞ্চমার্ক
GPU বনাম CPU বেঞ্চমার্ক

এই নিবন্ধটি WebGPU অন্বেষণের আমার যাত্রার শুরু মাত্র। GPU কম্পিউটে এবং রেন্ডারিং (ক্যানভাস, টেক্সচার, স্যাম্পলার) WebGPU-তে কীভাবে কাজ করে সে সম্পর্কে শীঘ্রই আরও বেশি নিবন্ধের প্রত্যাশা করুন।

,

এই পোস্টটি উদাহরণের মাধ্যমে পরীক্ষামূলক WebGPU API অন্বেষণ করে এবং আপনাকে GPU ব্যবহার করে ডেটা-সমান্তরাল গণনা করা শুরু করতে সাহায্য করে।

ফ্রাঁসোয়া বিউফোর্ট
François Beaufort

পটভূমি

আপনি ইতিমধ্যে জানেন যে, গ্রাফিক প্রসেসিং ইউনিট (GPU) হল একটি কম্পিউটারের মধ্যে একটি ইলেকট্রনিক সাবসিস্টেম যা মূলত গ্রাফিক্স প্রক্রিয়াকরণের জন্য বিশেষায়িত ছিল। যাইহোক, বিগত 10 বছরে, এটি একটি আরও নমনীয় আর্কিটেকচারের দিকে বিকশিত হয়েছে যা ডেভেলপারদের GPU-এর অনন্য আর্কিটেকচারের সুবিধা নেওয়ার সময়, শুধুমাত্র 3D গ্রাফিক্স রেন্ডার না করে, অনেক ধরনের অ্যালগরিদম বাস্তবায়ন করতে দেয়। এই ক্ষমতাগুলিকে জিপিইউ কম্পিউট হিসাবে উল্লেখ করা হয় এবং সাধারণ-উদ্দেশ্য বৈজ্ঞানিক কম্পিউটিংয়ের জন্য একটি জিপিইউকে একটি সহপ্রসেসর হিসাবে ব্যবহার করাকে সাধারণ-উদ্দেশ্যের জিপিইউ (GPGPU) প্রোগ্রামিং বলা হয়।

GPU Compute সাম্প্রতিক মেশিন লার্নিং বুমে উল্লেখযোগ্যভাবে অবদান রেখেছে, কারণ কনভোলিউশন নিউরাল নেটওয়ার্ক এবং অন্যান্য মডেলগুলি GPU-তে আরও দক্ষতার সাথে চালানোর জন্য আর্কিটেকচারের সুবিধা নিতে পারে। বর্তমান ওয়েব প্ল্যাটফর্মে GPU কম্পিউট ক্ষমতার অভাব থাকায়, W3C-এর "ওয়েবের জন্য GPU" কমিউনিটি গ্রুপ একটি এপিআই ডিজাইন করছে যাতে আধুনিক GPU এপিআইগুলিকে প্রকাশ করা যায় যা বেশিরভাগ বর্তমান ডিভাইসে উপলব্ধ। এই API কে WebGPU বলা হয়।

WebGPU হল একটি নিম্ন-স্তরের API, যেমন WebGL। এটা খুবই শক্তিশালী এবং বেশ শব্দসমৃদ্ধ, আপনি দেখতে পাবেন। কিন্তু যে ঠিক আছে. আমরা যা খুঁজছি তা হল কর্মক্ষমতা।

এই নিবন্ধে, আমি WebGPU-এর GPU কম্পিউট অংশে ফোকাস করতে যাচ্ছি এবং সত্যি কথা বলতে, আমি শুধু সারফেস স্ক্র্যাচ করছি, যাতে আপনি নিজে থেকে খেলা শুরু করতে পারেন। আমি আরও গভীরে ডুব দেব এবং আসন্ন নিবন্ধগুলিতে WebGPU রেন্ডারিং (ক্যানভাস, টেক্সচার, ইত্যাদি) কভার করব।

GPU অ্যাক্সেস করুন

WebGPU-তে GPU অ্যাক্সেস করা সহজ। navigator.gpu.requestAdapter() কল করা একটি জাভাস্ক্রিপ্ট প্রতিশ্রুতি প্রদান করে যা একটি GPU অ্যাডাপ্টারের সাথে অ্যাসিঙ্ক্রোনাসভাবে সমাধান করবে। এই অ্যাডাপ্টারটিকে গ্রাফিক্স কার্ড হিসাবে ভাবুন। এটি হয় ইন্টিগ্রেটেড হতে পারে (CPU-এর মতো একই চিপে) অথবা বিচ্ছিন্ন (সাধারণত একটি PCIe কার্ড যা বেশি পারফরম্যান্স কিন্তু বেশি শক্তি ব্যবহার করে)।

একবার আপনার কাছে GPU অ্যাডাপ্টার হয়ে গেলে, একটি প্রতিশ্রুতি পেতে adapter.requestDevice() কল করুন যা একটি GPU ডিভাইসের সাথে সমাধান করবে যা আপনি কিছু GPU গণনা করতে ব্যবহার করবেন।

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();

উভয় ফাংশন বিকল্পগুলি গ্রহণ করে যা আপনাকে অ্যাডাপ্টার (পাওয়ার পছন্দ) এবং ডিভাইস (এক্সটেনশন, সীমা) সম্পর্কে নির্দিষ্ট হতে দেয়। সরলতার জন্য, আমরা এই নিবন্ধে ডিফল্ট বিকল্পগুলি ব্যবহার করব।

বাফার মেমরি লিখুন

GPU-এর জন্য মেমরিতে ডেটা লেখার জন্য কীভাবে JavaScript ব্যবহার করবেন তা দেখা যাক। আধুনিক ওয়েব ব্রাউজারে ব্যবহৃত স্যান্ডবক্সিং মডেলের কারণে এই প্রক্রিয়াটি সহজবোধ্য নয়।

নীচের উদাহরণটি আপনাকে দেখায় কিভাবে GPU থেকে অ্যাক্সেসযোগ্য মেমরি বাফার করতে চার বাইট লিখতে হয়। এটি device.createBuffer() কল করে যা বাফারের আকার এবং এর ব্যবহার নেয়। যদিও ব্যবহার পতাকা GPUBufferUsage.MAP_WRITE এই নির্দিষ্ট কলের জন্য প্রয়োজন হয় না, আসুন আমরা এই বাফারে লিখতে চাই। এটির ফলে একটি GPU বাফার অবজেক্ট তৈরির সময় ম্যাপ করা হয় mappedAtCreation সত্যে সেট করা হয়েছে। তারপর GPU বাফার পদ্ধতি getMappedRange() কল করে সংশ্লিষ্ট কাঁচা বাইনারি ডেটা বাফারটি পুনরুদ্ধার করা যেতে পারে।

আপনি ইতিমধ্যে ArrayBuffer এর সাথে খেলে থাকলে বাইট লেখা পরিচিত; একটি TypedArray ব্যবহার করুন এবং এতে মানগুলি অনুলিপি করুন।

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE
});
const arrayBuffer = gpuBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

এই মুহুর্তে, GPU বাফারটি ম্যাপ করা হয়, যার অর্থ এটি CPU এর মালিকানাধীন, এবং এটি JavaScript থেকে রিড/রাইটে অ্যাক্সেসযোগ্য। যাতে GPU এটি অ্যাক্সেস করতে পারে, এটিকে আনম্যাপ করতে হবে যা gpuBuffer.unmap() কল করার মতোই সহজ।

ম্যাপড/আনম্যাপড ধারণাটি রেসের অবস্থা প্রতিরোধ করার জন্য প্রয়োজন যেখানে জিপিইউ এবং সিপিইউ একই সময়ে মেমরি অ্যাক্সেস করে।

বাফার মেমরি পড়ুন

এখন দেখা যাক কিভাবে একটি জিপিইউ বাফারকে অন্য একটি জিপিইউ বাফারে কপি করা যায় এবং তা আবার পড়তে হয়।

যেহেতু আমরা প্রথম GPU বাফারে লিখছি এবং আমরা এটিকে একটি দ্বিতীয় GPU বাফারে অনুলিপি করতে চাই, একটি নতুন ব্যবহারের পতাকা GPUBufferUsage.COPY_SRC প্রয়োজন৷ device.createBuffer() দিয়ে এবার দ্বিতীয় GPU বাফারটি একটি আনম্যাপড অবস্থায় তৈরি করা হয়েছে। এর ব্যবহারের পতাকা হল GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ যেহেতু এটি প্রথম GPU বাফারের গন্তব্য হিসাবে ব্যবহার করা হবে এবং GPU কপি কমান্ডগুলি কার্যকর করা হয়ে গেলে জাভাস্ক্রিপ্টে পড়বে৷

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuWriteBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC
});
const arrayBuffer = gpuWriteBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

// Unmap buffer so that it can be used later for copy.
gpuWriteBuffer.unmap();

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: 4,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

যেহেতু GPU একটি স্বাধীন কপ্রসেসর, সমস্ত GPU কমান্ড অ্যাসিঙ্ক্রোনাসভাবে কার্যকর করা হয়। এই কারণেই সেখানে GPU কমান্ডের একটি তালিকা তৈরি করা হয়েছে এবং প্রয়োজনের সময় ব্যাচে পাঠানো হয়েছে। WebGPU-তে, device.createCommandEncoder() দ্বারা প্রত্যাবর্তিত GPU কমান্ড এনকোডার হল জাভাস্ক্রিপ্ট অবজেক্ট যা "বাফার করা" কমান্ডের একটি ব্যাচ তৈরি করে যা কিছু সময়ে GPU-তে পাঠানো হবে। অন্যদিকে, GPUBuffer এর পদ্ধতিগুলি "আনবাফার" হয়, যার অর্থ তারা যখন বলা হয় তখন পারমাণবিকভাবে চালায়।

একবার আপনার কাছে GPU কমান্ড এনকোডার হয়ে গেলে, পরবর্তী কার্যকর করার জন্য কমান্ড সারিতে এই কমান্ডটি যুক্ত করতে নীচের দেখানো হিসাবে copyEncoder.copyBufferToBuffer() এ কল করুন। অবশেষে, copyEncoder.finish() কল করে এনকোডিং কমান্ড শেষ করুন এবং সেগুলিকে GPU ডিভাইস কমান্ড কিউতে জমা দিন। আর্গুমেন্ট হিসাবে জিপিইউ কমান্ড সহ device.queue.submit() মাধ্যমে জমা দেওয়া সাবমিট পরিচালনার জন্য সারি দায়ী। এটি পরমাণুভাবে অ্যারেতে সংরক্ষিত সমস্ত কমান্ডকে ক্রমানুসারে কার্যকর করবে।

// Encode commands for copying buffer to buffer.
const copyEncoder = device.createCommandEncoder();
copyEncoder.copyBufferToBuffer(
  gpuWriteBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  4 /* size */
);

// Submit copy commands.
const copyCommands = copyEncoder.finish();
device.queue.submit([copyCommands]);

এই মুহুর্তে, GPU সারি কমান্ড পাঠানো হয়েছে, কিন্তু অগত্যা কার্যকর করা হয়নি। দ্বিতীয় GPU বাফার পড়তে, GPUMapMode.READ এর সাথে gpuReadBuffer.mapAsync() কল করুন। এটি একটি প্রতিশ্রুতি প্রদান করে যা GPU বাফার ম্যাপ করা হলে সমাধান করবে। তারপর gpuReadBuffer.getMappedRange() দিয়ে ম্যাপ করা রেঞ্জটি পান যেটিতে প্রথম GPU বাফারের মতো একই মান রয়েছে একবার সমস্ত সারিবদ্ধ GPU কমান্ড কার্যকর করা হয়েছে।

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const copyArrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Uint8Array(copyArrayBuffer));

আপনি এই নমুনা চেষ্টা করতে পারেন.

সংক্ষেপে, বাফার মেমরি অপারেশন সম্পর্কে আপনার যা মনে রাখা দরকার তা এখানে:

  • ডিভাইস সারি জমাতে ব্যবহার করার জন্য GPU বাফারগুলিকে আনম্যাপ করতে হবে।
  • ম্যাপ করা হলে, জিপিইউ বাফারগুলি জাভাস্ক্রিপ্টে পড়া এবং লেখা যায়।
  • mapAsync() এবং mappedAtCreation সত্য হিসাবে সেট করে createBuffer() যখন জিপিইউ বাফারগুলি ম্যাপ করা হয়।

শেডার প্রোগ্রামিং

জিপিইউতে চলমান প্রোগ্রামগুলি যা কেবল গণনা সম্পাদন করে (এবং ত্রিভুজগুলি আঁকেন না) তাকে কমপুট শেডার বলা হয়। এগুলি শত শত জিপিইউ কোর (যা সিপিইউ কোরের চেয়ে ছোট) দ্বারা সমান্তরালে কার্যকর করা হয় যা ডেটা ক্রাচ করতে একসাথে কাজ করে। তাদের ইনপুট এবং আউটপুট ওয়েবজিপিইউতে বাফার।

ওয়েবজিপিইউতে গণনা শেডারগুলির ব্যবহার চিত্রিত করতে, আমরা নীচে চিত্রিত মেশিন লার্নিংয়ের একটি সাধারণ অ্যালগরিদম ম্যাট্রিক্স গুণনের সাথে খেলব।

ম্যাট্রিক্স গুণক চিত্র
ম্যাট্রিক্স গুণক চিত্র

সংক্ষেপে, আমরা যা করতে যাচ্ছি তা এখানে:

  1. তিনটি জিপিইউ বাফার তৈরি করুন (ম্যাট্রিক্সের জন্য দুটি এবং ফলাফল ম্যাট্রিক্সের জন্য একটি)
  2. গণনা শেডারের জন্য ইনপুট এবং আউটপুট বর্ণনা করুন
  3. গণনা শেডার কোড সংকলন করুন
  4. একটি গণনা পাইপলাইন সেট আপ করুন
  5. জিপিইউতে এনকোডেড কমান্ডগুলি ব্যাচে জমা দিন
  6. ফলাফল ম্যাট্রিক্স জিপিইউ বাফার পড়ুন

জিপিইউ বাফার তৈরি

সরলতার জন্য, ম্যাট্রিকগুলি ভাসমান পয়েন্ট সংখ্যার তালিকা হিসাবে প্রতিনিধিত্ব করা হবে। প্রথম উপাদানটি হ'ল সারিগুলির সংখ্যা, দ্বিতীয় উপাদানটি কলামগুলির সংখ্যা এবং বাকীটি ম্যাট্রিক্সের আসল সংখ্যা।

জাভাস্ক্রিপ্টে একটি ম্যাট্রিক্সের সহজ উপস্থাপনা এবং গাণিতিক স্বরলিপিতে এর সমতুল্য
জাভাস্ক্রিপ্টে একটি ম্যাট্রিক্সের সহজ উপস্থাপনা এবং গাণিতিক স্বরলিপিতে এর সমতুল্য

তিনটি জিপিইউ বাফার হ'ল স্টোরেজ বাফার কারণ আমাদের গণনা শেডারে ডেটা সংরক্ষণ এবং পুনরুদ্ধার করতে হবে। এটি ব্যাখ্যা করে যে জিপিইউ বাফার ব্যবহারের পতাকাগুলিতে কেন তাদের সকলের জন্য GPUBufferUsage.STORAGE অন্তর্ভুক্ত রয়েছে। ফলাফল ম্যাট্রিক্স ব্যবহারের পতাকাটিতে GPUBufferUsage.COPY_SRC রয়েছে কারণ সমস্ত জিপিইউ কুইউ কমান্ডগুলি কার্যকর করা হয়ে গেলে এটি পড়ার জন্য এটি অন্য একটি বাফারে অনুলিপি করা হবে।

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();


// First Matrix

const firstMatrix = new Float32Array([
  2 /* rows */, 4 /* columns */,
  1, 2, 3, 4,
  5, 6, 7, 8
]);

const gpuBufferFirstMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: firstMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferFirstMatrix = gpuBufferFirstMatrix.getMappedRange();
new Float32Array(arrayBufferFirstMatrix).set(firstMatrix);
gpuBufferFirstMatrix.unmap();


// Second Matrix

const secondMatrix = new Float32Array([
  4 /* rows */, 2 /* columns */,
  1, 2,
  3, 4,
  5, 6,
  7, 8
]);

const gpuBufferSecondMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: secondMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferSecondMatrix = gpuBufferSecondMatrix.getMappedRange();
new Float32Array(arrayBufferSecondMatrix).set(secondMatrix);
gpuBufferSecondMatrix.unmap();


// Result Matrix

const resultMatrixBufferSize = Float32Array.BYTES_PER_ELEMENT * (2 + firstMatrix[0] * secondMatrix[1]);
const resultMatrixBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});

বাইন্ড গ্রুপ লেআউট এবং বাইন্ড গ্রুপ

বাইন্ড গ্রুপ লেআউট এবং বাইন্ড গ্রুপের ধারণাগুলি ওয়েবজিপিইউর জন্য নির্দিষ্ট। একটি বাইন্ড গ্রুপ লেআউট কোনও শেডার দ্বারা প্রত্যাশিত ইনপুট/আউটপুট ইন্টারফেসকে সংজ্ঞায়িত করে, যখন একটি বাইন্ড গ্রুপ কোনও শেডারের জন্য প্রকৃত ইনপুট/আউটপুট ডেটা উপস্থাপন করে।

নীচের উদাহরণে, বাইন্ড গ্রুপ লেআউটটি সংখ্যাযুক্ত এন্ট্রি বাইন্ডিং 0 , 1 এ দুটি পঠনযোগ্য স্টোরেজ বাফার এবং কম্পিউটারের শেডারের জন্য 2 এ স্টোরেজ বাফার আশা করে। অন্যদিকে বাইন্ড গ্রুপটি, এই বাইন্ড গ্রুপের বিন্যাসের জন্য সংজ্ঞায়িত, জিপিইউ বাফারগুলি এন্ট্রিগুলির সাথে সংযুক্ত করে: gpuBufferFirstMatrix বাইন্ডিং 0 , gpuBufferSecondMatrix বাইন্ডিং 1 এর সাথে এবং বাইন্ডিং 2 তে resultMatrixBuffer

const bindGroupLayout = device.createBindGroupLayout({
  entries: [
    {
      binding: 0,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "read-only-storage"
      }
    },
    {
      binding: 1,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "read-only-storage"
      }
    },
    {
      binding: 2,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "storage"
      }
    }
  ]
});

const bindGroup = device.createBindGroup({
  layout: bindGroupLayout,
  entries: [
    {
      binding: 0,
      resource: {
        buffer: gpuBufferFirstMatrix
      }
    },
    {
      binding: 1,
      resource: {
        buffer: gpuBufferSecondMatrix
      }
    },
    {
      binding: 2,
      resource: {
        buffer: resultMatrixBuffer
      }
    }
  ]
});

গণনা শেডার কোড

গুনে ম্যাট্রিক্সের জন্য গণনা শেডার কোডটি ডাব্লুজিএসএলে , ওয়েবজিপিইউ শেডার ভাষায় লেখা হয়েছে, এটি স্পির-ভি -তে তুচ্ছভাবে অনুবাদযোগ্য। বিশদে না গিয়ে আপনার var<storage> দিয়ে চিহ্নিত তিনটি স্টোরেজ বাফার নীচে সন্ধান করা উচিত। প্রোগ্রামটি firstMatrix এবং secondMatrix ইনপুট হিসাবে এবং resultMatrix আউটপুট হিসাবে ব্যবহার করবে।

নোট করুন যে প্রতিটি স্টোরেজ বাফারের একটি binding সজ্জা ব্যবহার করা হয় যা বাইন্ড গ্রুপ লেআউট এবং উপরে বর্ণিত বাইন্ড গ্রুপগুলিতে সংজ্ঞায়িত একই সূচকের সাথে মিলে যায়।

const shaderModule = device.createShaderModule({
  code: `
    struct Matrix {
      size : vec2f,
      numbers: array<f32>,
    }

    @group(0) @binding(0) var<storage, read> firstMatrix : Matrix;
    @group(0) @binding(1) var<storage, read> secondMatrix : Matrix;
    @group(0) @binding(2) var<storage, read_write> resultMatrix : Matrix;

    @compute @workgroup_size(8, 8)
    fn main(@builtin(global_invocation_id) global_id : vec3u) {
      // Guard against out-of-bounds work group sizes
      if (global_id.x >= u32(firstMatrix.size.x) || global_id.y >= u32(secondMatrix.size.y)) {
        return;
      }

      resultMatrix.size = vec2(firstMatrix.size.x, secondMatrix.size.y);

      let resultCell = vec2(global_id.x, global_id.y);
      var result = 0.0;
      for (var i = 0u; i < u32(firstMatrix.size.y); i = i + 1u) {
        let a = i + resultCell.x * u32(firstMatrix.size.y);
        let b = resultCell.y + i * u32(secondMatrix.size.y);
        result = result + firstMatrix.numbers[a] * secondMatrix.numbers[b];
      }

      let index = resultCell.y + resultCell.x * u32(secondMatrix.size.y);
      resultMatrix.numbers[index] = result;
    }
  `
});

পাইপলাইন সেটআপ

গণনা পাইপলাইনটি এমন একটি অবজেক্ট যা আসলে আমরা সম্পাদন করতে যাচ্ছি এমন গণনা অপারেশন বর্ণনা করে। device.createComputePipeline() এটি দুটি যুক্তি নেয়: আমরা আগে তৈরি করা বাইন্ড গ্রুপ লেআউট এবং আমাদের গণনা শেডার ( main ডাব্লুজিএসএল ফাংশন) এর প্রবেশ বিন্দু এবং device.createShaderModule() দিয়ে তৈরি প্রকৃত গণনা শেডার মডিউলটির এন্ট্রি পয়েন্ট সংজ্ঞায়িত একটি গণনা পর্যায়।

const computePipeline = device.createComputePipeline({
  layout: device.createPipelineLayout({
    bindGroupLayouts: [bindGroupLayout]
  }),
  compute: {
    module: shaderModule,
    entryPoint: "main"
  }
});

কমান্ড জমা দেওয়া

আমাদের তিনটি জিপিইউ বাফার এবং একটি বাইন্ড গ্রুপ লেআউট সহ একটি গণনা পাইপলাইন দিয়ে একটি বাইন্ড গ্রুপ ইনস্ট্যান্ট করার পরে, সেগুলি ব্যবহার করার সময় এসেছে।

আসুন commandEncoder.beginComputePass() এর সাথে একটি প্রোগ্রামেবল কম্পিউট পাস এনকোডার শুরু করি। আমরা এটি জিপিইউ কমান্ডগুলি এনকোড করতে ব্যবহার করব যা ম্যাট্রিক্স গুণটি সম্পাদন করবে। passEncoder.setPipeline(computePipeline) এবং এর বাইন্ড গ্রুপের সাথে এর পাইপলাইন সেট করুন passEncoder.setBindGroup(0, bindGroup) এর সাথে সূচক 0 এ। সূচক 0 ডাব্লুজিএসএল কোডে group(0) সজ্জার সাথে মিলে যায়।

এখন, আসুন এই গণনা শেডারটি কীভাবে জিপিইউতে চলতে চলেছে সে সম্পর্কে কথা বলি। আমাদের লক্ষ্য হ'ল ধাপে ধাপে ম্যাট্রিক্সের প্রতিটি কক্ষের সমান্তরালে এই প্রোগ্রামটি সম্পাদন করা। উদাহরণস্বরূপ 32 দ্বারা আকারের 16 আকারের ম্যাট্রিক্সের জন্য, @workgroup_size(8, 8) এ এক্সিকিউশন কমান্ডটি এনকোড করার জন্য, আমরা passEncoder.dispatchWorkgroups(2, 4) বা passEncoder.dispatchWorkgroups(16 / 8, 32 / 8) । প্রথম যুক্তি "এক্স" প্রথম মাত্রা, দ্বিতীয়টি "ওয়াই" দ্বিতীয় মাত্রা এবং সর্বশেষতম একটি "জেড" তৃতীয় মাত্রা যা আমাদের এখানে প্রয়োজন হয় না বলে 1 এর ডিফল্ট হয়। জিপিইউ কম্পিউটারের ওয়ার্ল্ডে, ডেটা সেটে কার্নেল ফাংশন কার্যকর করার জন্য একটি কমান্ড এনকোডিংকে প্রেরণ করা হয়।

প্রতিটি ফলাফল ম্যাট্রিক্স সেল জন্য সমান্তরালভাবে সম্পাদন
প্রতিটি ফলাফল ম্যাট্রিক্স সেল জন্য সমান্তরালভাবে সম্পাদন

আমাদের গণনা শেডারের জন্য ওয়ার্কগ্রুপ গ্রিডের আকারটি আমাদের ডাব্লুজিএসএল কোডে (8, 8) । তার কারণে, "এক্স" এবং "ওয়াই" যা যথাক্রমে প্রথম ম্যাট্রিক্সের সারিগুলির সংখ্যা এবং দ্বিতীয় ম্যাট্রিক্সের কলামগুলির সংখ্যা 8 দ্বারা বিভক্ত করা হবে। এটির সাথে আমরা এখন passEncoder.dispatchWorkgroups(firstMatrix[0] / 8, secondMatrix[1] / 8) । চালানোর জন্য ওয়ার্কগ্রুপ গ্রিডের সংখ্যা হ'ল dispatchWorkgroups() যুক্তি।

উপরের অঙ্কনে যেমন দেখা গেছে, প্রতিটি শেডারের একটি অনন্য builtin(global_invocation_id) অবজেক্টে অ্যাক্সেস থাকবে যা গণনা করতে কোন ফলাফল ম্যাট্রিক্স সেলটি জানতে ব্যবহৃত হবে।

const commandEncoder = device.createCommandEncoder();

const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
const workgroupCountX = Math.ceil(firstMatrix[0] / 8);
const workgroupCountY = Math.ceil(secondMatrix[1] / 8);
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY);
passEncoder.end();

গণনা পাস এনকোডারটি শেষ করতে, কল করুন passEncoder.end() । তারপরে, অনুলিপি ম্যাট্রিক্স বাফারটি অনুলিপি copyBufferToBuffer জন্য গন্তব্য হিসাবে ব্যবহার করার জন্য একটি জিপিইউ বাফার তৈরি করুন। শেষ অবধি, copyEncoder.finish() এর সাথে এনকোডিং কমান্ডগুলি শেষ করুন এবং জিপিইউ ডিভাইস কাতারে জমা দিন device.queue.submit() জিপিইউ কমান্ডগুলির সাথে কল করে জমা দিন।

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

// Encode commands for copying buffer to buffer.
commandEncoder.copyBufferToBuffer(
  resultMatrixBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  resultMatrixBufferSize /* size */
);

// Submit GPU commands.
const gpuCommands = commandEncoder.finish();
device.queue.submit([gpuCommands]);

ফলাফল ম্যাট্রিক্স পড়ুন

ফলাফল ম্যাট্রিক্স পড়া gpuReadBuffer.mapAsync() এর সাথে GPUMapMode.READ সাথে কল করার মতো সহজ। এই মুহুর্তে, gpuReadBuffer.getMappedRange() এর সাথে ম্যাপযুক্ত পরিসীমা পাওয়া সম্ভব।

ম্যাট্রিক্স গুণনের ফলাফল
ম্যাট্রিক্স গুণনের ফলাফল

আমাদের কোডে, ডেভটুলস জাভাস্ক্রিপ্ট কনসোলে লগ করা ফলাফলটি "2, 2, 50, 60, 114, 140"।

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const arrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Float32Array(arrayBuffer));

অভিনন্দন! আপনি এটা তৈরি করেছেন. আপনি নমুনা দিয়ে খেলতে পারেন।

একটি শেষ কৌশল

আপনার কোডটি পড়তে সহজ করার একটি উপায় হ'ল শেডার মডিউল থেকে বাইন্ড গ্রুপ লেআউটটি নির্ধারণের জন্য গণনা পাইপলাইনের হ্যান্ডি getBindGroupLayout পদ্ধতিটি ব্যবহার করা। এই কৌশলটি কাস্টম বাইন্ড গ্রুপ লেআউট তৈরি করা এবং আপনার কম্পিউটারের পাইপলাইনে একটি পাইপলাইন লেআউট নির্দিষ্ট করে আপনি নীচে দেখতে পারেন এমন প্রয়োজনীয়তা সরিয়ে দেয়।

পূর্ববর্তী নমুনার জন্য getBindGroupLayout এর একটি চিত্র উপলব্ধ

 const computePipeline = device.createComputePipeline({
-  layout: device.createPipelineLayout({
-    bindGroupLayouts: [bindGroupLayout]
-  }),
   compute: {
-// Bind group layout and bind group
- const bindGroupLayout = device.createBindGroupLayout({
-   entries: [
-     {
-       binding: 0,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "read-only-storage"
-       }
-     },
-     {
-       binding: 1,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "read-only-storage"
-       }
-     },
-     {
-       binding: 2,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "storage"
-       }
-     }
-   ]
- });
+// Bind group
  const bindGroup = device.createBindGroup({
-  layout: bindGroupLayout,
+  layout: computePipeline.getBindGroupLayout(0 /* index */),
   entries: [

পারফরম্যান্স অনুসন্ধান

তাহলে কীভাবে কোনও জিপিইউতে ম্যাট্রিক্স গুণন চলমান এটি সিপিইউতে চালনার সাথে তুলনা করে? এটি জানতে, আমি সিপিইউর জন্য বর্ণিত প্রোগ্রামটি লিখেছি। এবং আপনি নীচের গ্রাফটিতে দেখতে পাচ্ছেন, জিপিইউর সম্পূর্ণ শক্তি ব্যবহার করা একটি সুস্পষ্ট পছন্দ বলে মনে হয় যখন ম্যাট্রিক্সের আকার 256 দ্বারা 256 এর চেয়ে বেশি হয়।

জিপিইউ বনাম সিপিইউ বেঞ্চমার্ক
জিপিইউ বনাম সিপিইউ বেঞ্চমার্ক

এই নিবন্ধটি ওয়েবজিপিইউ অন্বেষণ করে আমার যাত্রার শুরু ছিল। জিপিইউ কম্পিউটারে আরও গভীর ডাইভ বৈশিষ্ট্যযুক্ত এবং আরও নিবন্ধগুলি কীভাবে রেন্ডারিং (ক্যানভাস, টেক্সচার, স্যাম্পলার) ওয়েবজিপিইউতে কাজ করে তার আরও নিবন্ধগুলি প্রত্যাশা করুন।