वेब पर जीपीयू कंप्यूट का इस्तेमाल शुरू करना

इस पोस्ट में, उदाहरणों की मदद से एक्सपेरिमेंट के तौर पर उपलब्ध WebGPU API के बारे में बताया गया है. साथ ही, जीपीयू का इस्तेमाल करके, डेटा-पаралल कैलकुलेशन करने के बारे में भी बताया गया है.

François Beaufort
François Beaufort

बैकग्राउंड

आपको पता ही होगा कि ग्राफ़िक प्रोसेसिंग यूनिट (जीपीयू), कंप्यूटर में मौजूद एक इलेक्ट्रॉनिक सबसिस्टम है. इसे मूल रूप से ग्राफ़िक्स प्रोसेस करने के लिए बनाया गया था. हालांकि, पिछले 10 सालों में, यह ज़्यादा फ़्लेक्सिबल आर्किटेक्चर में बदल गया है. इससे डेवलपर, जीपीयू के यूनीक आर्किटेक्चर का फ़ायदा लेते हुए, 3D ग्राफ़िक्स को रेंडर करने के साथ-साथ कई तरह के एल्गोरिदम लागू कर सकते हैं. इन सुविधाओं को जीपीयू कंप्यूट कहा जाता है. साथ ही, सामान्य वैज्ञानिक कंप्यूटिंग के लिए, जीपीयू को कोप्रोसेसर के तौर पर इस्तेमाल करने को सामान्य काम के लिए जीपीयू (जीपीजीपीयू) प्रोग्रामिंग कहा जाता है.

जीपीयू कंप्यूट ने हाल ही में मशीन लर्निंग के क्षेत्र में हुए उछाल में काफ़ी योगदान दिया है. ऐसा इसलिए, क्योंकि कन्वोल्यूशन न्यूरल नेटवर्क और दूसरे मॉडल, जीपीयू पर ज़्यादा बेहतर तरीके से काम करने के लिए, आर्किटेक्चर का फ़ायदा ले सकते हैं. मौजूदा वेब प्लैटफ़ॉर्म में जीपीयू कंप्यूट की सुविधाएं मौजूद नहीं हैं. इसलिए, W3C का "वेब के लिए जीपीयू" कम्यूनिटी ग्रुप, एक एपीआई डिज़ाइन कर रहा है. इससे, वे आधुनिक जीपीयू एपीआई दिखाए जा सकेंगे जो ज़्यादातर मौजूदा डिवाइसों पर उपलब्ध हैं. इस एपीआई को WebGPU कहा जाता है.

WebGPU, WebGL की तरह ही एक लो-लेवल एपीआई है. यह बहुत ज़्यादा जानकारी देने वाला और बेहतरीन तरीका है, जैसा कि आपको दिखेगा. हालांकि, इसमें कोई समस्या नहीं है. हम परफ़ॉर्मेंस पर ध्यान देते हैं.

इस लेख में, मैं WebGPU के GPU Compute वाले हिस्से पर फ़ोकस करूंगा. सच कहूं, तो मैं सिर्फ़ इस बारे में बता रहा हूं, ताकि आप खुद इस बारे में जान सकें. आने वाले लेखों में, मैं WebGPU रेंडरिंग (कैनवस, टेक्स्चर वगैरह) के बारे में ज़्यादा जानकारी दूंगा.

जीपीयू को ऐक्सेस करना

WebGPU में जीपीयू को ऐक्सेस करना आसान है. navigator.gpu.requestAdapter() को कॉल करने पर, एक JavaScript प्रॉमिस मिलता है. यह प्रॉमिस, GPU अडैप्टर की मदद से एसिंक्रोनस तरीके से पूरा होगा. इस अडैप्टर को ग्राफ़िक्स कार्ड के तौर पर समझें. यह इंटिग्रेट किया गया (सीपीयू के साथ एक ही चिप पर) या डिसक्रीट (आम तौर पर, बेहतर परफ़ॉर्मेंस वाला PCIe कार्ड, जो ज़्यादा बिजली का इस्तेमाल करता है) हो सकता है.

जीपीयू अडैप्टर मिलने के बाद, adapter.requestDevice() को कॉल करके एक ऐसा प्रॉमिस पाएं जो जीपीयू डिवाइस के साथ रिज़ॉल्व होगा. इसका इस्तेमाल, जीपीयू पर कुछ कैलकुलेशन करने के लिए किया जाएगा.

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

दोनों फ़ंक्शन में आपको ऐसे विकल्प मिलते हैं जिनकी मदद से, अपनी पसंद के अडैप्टर (बिजली की प्राथमिकता) और डिवाइस (एक्सटेंशन, सीमाएं) के बारे में बताया जा सकता है. आसानी से समझाने के लिए, हम इस लेख में डिफ़ॉल्ट विकल्पों का इस्तेमाल करेंगे.

बफ़र मेमोरी में डेटा लिखना

आइए, देखें कि GPU की मेमोरी में डेटा लिखने के लिए, JavaScript का इस्तेमाल कैसे किया जाता है. यह प्रोसेस आसान नहीं है, क्योंकि आधुनिक वेब ब्राउज़र में सैंडबॉक्सिंग मॉडल का इस्तेमाल किया जाता है.

नीचे दिए गए उदाहरण में, जीपीयू से ऐक्सेस की जा सकने वाली बफ़र मेमोरी में चार बाइट लिखने का तरीका बताया गया है. यह device.createBuffer() को कॉल करता है, जो बफ़र का साइज़ और उसका इस्तेमाल तय करता है. भले ही, इस कॉल के लिए इस्तेमाल के फ़्लैग GPUBufferUsage.MAP_WRITE की ज़रूरत नहीं है, लेकिन हम साफ़ तौर पर बताना चाहते हैं कि हमें इस बफ़र में लिखना है. इसकी वजह से, mappedAtCreation को 'सही' पर सेट करने की वजह से, बनाने के दौरान मैप किया गया एक GPU बफ़र ऑब्जेक्ट बन जाता है. इसके बाद, 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]);

इस समय, जीपीयू बफ़र को मैप किया जाता है. इसका मतलब है कि इसका मालिकाना हक सीपीयू के पास होता है और इसे JavaScript से रीड/राइट किया जा सकता है. जीपीयू इसे ऐक्सेस कर सके, इसके लिए इसे अनमैप करना होगा. ऐसा करना उतना ही आसान है जितना gpuBuffer.unmap() को कॉल करना.

मैप किए गए/अनमैप किए गए कॉन्सेप्ट की ज़रूरत, रेस कंडीशन से बचने के लिए होती है. रेस कंडीशन में, जीपीयू और सीपीयू एक ही समय पर मेमोरी को ऐक्सेस करते हैं.

बफ़र मेमोरी पढ़ें

अब देखते हैं कि किसी GPU बफ़र को किसी दूसरे GPU बफ़र में कॉपी करने और उसे वापस पढ़ने का तरीका क्या है.

हम पहले जीपीयू बफ़र में लिख रहे हैं और हमें इसे दूसरे जीपीयू बफ़र में कॉपी करना है. इसलिए, इस्तेमाल से जुड़ा नया फ़्लैग GPUBufferUsage.COPY_SRC ज़रूरी है. इस बार, दूसरा GPU बफ़र device.createBuffer() के साथ, बिना मैप किए गए स्टेटस में बनाया गया है. इसका इस्तेमाल फ़्लैग GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ है, क्योंकि इसका इस्तेमाल पहले GPU बफ़र के डेस्टिनेशन के तौर पर किया जाएगा. साथ ही, GPU कॉपी निर्देशों के लागू होने के बाद, इसे JavaScript में पढ़ा जाएगा.

// 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 के सभी निर्देश एक साथ नहीं, बल्कि अलग-अलग समय पर लागू होते हैं. इसलिए, जीपीयू के निर्देशों की एक सूची बनाई जाती है और ज़रूरत पड़ने पर, उन्हें एक साथ भेजा जाता है. WebGPU में, device.createCommandEncoder()से मिलने वाला जीपीयू कमांड एन्कोडर, एक JavaScript ऑब्जेक्ट है. यह "बफ़र किए गए" कमांड का एक बैच बनाता है, जिसे किसी समय जीपीयू पर भेजा जाएगा. दूसरी ओर, GPUBuffer के तरीके "बफ़र नहीं किए जाते". इसका मतलब है कि उन्हें बुलाए जाने के समय, वे एक साथ काम करते हैं.

जीपीयू कमांड एन्कोडर मिलने के बाद, इस कमांड को कमांड कतार में जोड़ने के लिए, 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 लाइन में मौजूद निर्देश भेजे जा चुके हैं, लेकिन ज़रूरी नहीं है कि वे लागू हो गए हों. दूसरा जीपीयू बफ़र पढ़ने के लिए, GPUMapMode.READ के साथ gpuReadBuffer.mapAsync() को कॉल करें. यह एक प्रॉमिस दिखाता है, जो जीपीयू बफ़र के मैप होने पर रिज़ॉल्व हो जाएगा. इसके बाद, gpuReadBuffer.getMappedRange() के साथ मैप की गई वह रेंज पाएं जिसमें, सूची में मौजूद सभी जीपीयू निर्देशों को पूरा करने के बाद, पहले जीपीयू बफ़र जैसी ही वैल्यू हों.

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

इस सैंपल को आज़माएं.

कम शब्दों में, बफ़र मेमोरी के काम करने के तरीके के बारे में आपको ये बातें याद रखनी होंगी:

  • डिवाइस की सूची सबमिट करने के लिए, GPU बफ़र को अनमैप करना होगा.
  • मैप किए जाने पर, GPU बफ़र को JavaScript में पढ़ा और लिखा जा सकता है.
  • mapAsync() और createBuffer() को mappedAtCreation के 'सही है' पर सेट होने पर कॉल करने पर, जीपीयू बफ़र मैप किए जाते हैं.

शेडर प्रोग्रामिंग

जीपीयू पर चलने वाले ऐसे प्रोग्राम जिन्हें सिर्फ़ कैलकुलेशन करने के लिए इस्तेमाल किया जाता है और जो ट्राइऐंगल नहीं बनाते उन्हें कंप्यूट शेडर कहा जाता है. ये निर्देश, सीपीयू कोर के मुकाबले छोटे होते हैं. ये सैकड़ों जीपीयू कोर के साथ एक साथ काम करते हैं और डेटा को प्रोसेस करते हैं. WebGPU में, इनका इनपुट और आउटपुट बफ़र होता है.

WebGPU में कंप्यूट शेडर के इस्तेमाल को दिखाने के लिए, हम मैट्रिक्स गुणा करने की सुविधा का इस्तेमाल करेंगे. यह मशीन लर्निंग में इस्तेमाल होने वाला एक सामान्य एल्गोरिदम है. इसकी जानकारी नीचे दी गई है.

मैट्रिक्स गुणन का डायग्राम
मैट्रिक्स गुणन का डायग्राम

कम शब्दों में, हम ये काम करने जा रहे हैं:

  1. तीन GPU बफ़र बनाएं (दो मैट्रिक्स के लिए, जिनका गुणा करना है और एक नतीजे वाली मैट्रिक के लिए)
  2. कंप्यूट शेडर के इनपुट और आउटपुट के बारे में बताना
  3. कंप्यूट शेडर कोड को कंपाइल करना
  4. कंप्यूट पाइपलाइन सेट अप करना
  5. कोड में बदले गए निर्देशों को जीपीयू पर एक साथ सबमिट करना
  6. नतीजे के मैट्रिक्स जीपीयू बफ़र को पढ़ना

जीपीयू बफ़र बनाना

आसानी से समझने के लिए, मैट्रिक्स को फ़्लोटिंग पॉइंट वाली संख्याओं की सूची के तौर पर दिखाया जाएगा. पहला एलिमेंट, पंक्तियों की संख्या है, दूसरा एलिमेंट कॉलम की संख्या है, और बाकी एलिमेंट मैट्रिक्स की असल संख्याएं हैं.

JavaScript में मैट्रिक को आसानी से दिखाने का तरीका और मैथमैटिकल नोटेशन में इसका बराबर
JavaScript में मैट्रिक्स को आसानी से दिखाने का तरीका और मैथमैटिकल नोटेशन में इसका बराबर वैल्यू

तीनों जीपीयू बफ़र, स्टोरेज बफ़र होते हैं, क्योंकि हमें कंप्यूट शेडर में डेटा को स्टोर और फिर से पाना होता है. इससे पता चलता है कि GPU बफ़र के इस्तेमाल के फ़्लैग में, उन सभी के लिए 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
});

ग्रुप लेआउट और ग्रुप बांधना

बांधने वाले ग्रुप के लेआउट और बांधने वाले ग्रुप के कॉन्सेप्ट, WebGPU के लिए खास हैं. बाइंड ग्रुप लेआउट, शेडर के लिए ज़रूरी इनपुट/आउटपुट इंटरफ़ेस तय करता है. वहीं, बाइंड ग्रुप, शेडर के लिए असल इनपुट/आउटपुट डेटा दिखाता है.

नीचे दिए गए उदाहरण में, बाइंड ग्रुप लेआउट में, संख्या वाली एंट्री बाइंडिंग 0, 1 पर दो रीड-ओनली स्टोरेज बफ़र और कंप्यूट शेडर के लिए 2 पर एक स्टोरेज बफ़र होना चाहिए. दूसरी ओर, इस बाइंड ग्रुप लेआउट के लिए तय किया गया बाइंड ग्रुप, जीपीयू बफ़र को एंट्री से जोड़ता है: gpuBufferFirstMatrix को बाइंडिंग 0 से, gpuBufferSecondMatrix को बाइंडिंग 1 से, और resultMatrixBuffer को बाइंडिंग 2 से.

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"
  }
});

निर्देश सबमिट करना

अपने तीन जीपीयू बफ़र और बाइंड ग्रुप लेआउट वाली कंप्यूट लाइन के साथ बाइंड ग्रुप को इंस्टैंशिएट करने के बाद, उनका इस्तेमाल किया जा सकता है.

आइए, commandEncoder.beginComputePass() की मदद से प्रोग्राम किया जा सकने वाला कंप्यूट पास एन्कोडर बनाना शुरू करें. हम इसका इस्तेमाल, जीपीयू के उन निर्देशों को कोड में बदलने के लिए करेंगे जो मैट्रिक का गुणा करेंगे. इसकी पाइपलाइन को passEncoder.setPipeline(computePipeline) और इसके बाइंड ग्रुप को इंडेक्स 0 पर passEncoder.setBindGroup(0, bindGroup) के साथ सेट करें. इंडेक्स 0, WGSL कोड में group(0) डेकोरेशन से जुड़ा होता है.

अब बात करते हैं कि यह कंप्यूट शेडर, जीपीयू पर कैसे काम करेगा. हमारा मकसद, नतीजे वाले मैट्रिक की हर सेल के लिए, इस प्रोग्राम को एक साथ, सिलसिलेवार तरीके से चलाना है. उदाहरण के लिए, 16 x 32 साइज़ के नतीजे वाले मैट्रिक्स के लिए, @workgroup_size(8, 8) पर, एक्सीक्यूशन के निर्देश को कोड में बदलने के लिए, हम passEncoder.dispatchWorkgroups(2, 4) या passEncoder.dispatchWorkgroups(16 / 8, 32 / 8) को कॉल करेंगे. पहला आर्ग्युमेंट "x" पहला डाइमेंशन है, दूसरा आर्ग्युमेंट "y" दूसरा डाइमेंशन है, और तीसरा आर्ग्युमेंट "z" तीसरा डाइमेंशन है. यह डिफ़ॉल्ट रूप से 1 पर सेट होता है, क्योंकि हमें इसकी ज़रूरत नहीं है. जीपीयू कंप्यूट की दुनिया में, डेटा के किसी सेट पर कर्नेल फ़ंक्शन को लागू करने के लिए, किसी निर्देश को कोड में बदलने की प्रोसेस को डिस्पैचिंग कहा जाता है.

नतीजे वाली हर मैट्रिक सेल के लिए, एक साथ कई फ़ंक्शन लागू करना
नतीजे वाली हर मैट्रिक्स सेल के लिए, एक साथ कई फ़ंक्शन चलाना

हमारे WGSL कोड में, हमारे कंप्यूट शेडर के लिए वर्कग्रुप ग्रिड का साइज़ (8, 8) है. इस वजह से, "x" और "y", जो पहले मैट्रिक्स की पंक्तियों की संख्या और दूसरे मैट्रिक्स के कॉलम की संख्या है, उनमें आठ से भाग किया जाएगा. इसके बाद, हम 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));

बधाई हो! आपने कर लिया. सैंपल के साथ खेला जा सकता है.

एक आखिरी तरकीब

अपने कोड को आसानी से पढ़ने के लिए, शेडर मॉड्यूल से बाइंड ग्रुप लेआउट का अनुमान लगाने के लिए, कैलकुलेट पाइपलाइन के 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 x 256 से ज़्यादा हो, तो जीपीयू की पूरी क्षमता का इस्तेमाल करना एक सही विकल्प है.

जीपीयू बनाम सीपीयू के मानदंड
जीपीयू बनाम सीपीयू बेंचमार्क

WebGPU को एक्सप्लोर करने की मेरी यात्रा की शुरुआत इस लेख से हुई. जल्द ही, जीपीयू कंप्यूट और WebGPU में रेंडरिंग (कैनवस, टेक्स्चर, सैंपलर) के काम करने के तरीके के बारे में ज़्यादा जानकारी देने वाले लेख उपलब्ध होंगे.