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

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

François Beaufort
François Beaufort

बैकग्राउंड

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

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

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

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

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

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

जीपीयू अडैप्टर मिलने के बाद, adapter.requestDevice() को कॉल करके ऐसा प्रॉमिस पाना जो जीपीयू डिवाइस से रिज़ॉल्व करने पर, जीपीयू की गणना करेगा.

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

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

बफ़र मेमोरी में बदलाव करें

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

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

जहां जीपीयू और सीपीयू (CPU) ऐक्सेस मेमोरी एक ही समय पर दी जाती है. ऐसी स्थितियों को रोकने के लिए मैप किए/अनमैप किए गए का सिद्धांत ज़रूरी है.

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

आइए, अब देखते हैं कि किसी जीपीयू बफ़र को किसी अन्य जीपीयू बफ़र में कैसे कॉपी किया जाता है और इसे कैसे पढ़ा जाता है.

हम पहले जीपीयू बफ़र में टेक्स्ट लिख रहे हैं और हम इसे दूसरे जीपीयू बफ़र में कॉपी करना चाहते हैं. इसलिए, आपको नए इस्तेमाल फ़्लैग GPUBufferUsage.COPY_SRC की ज़रूरत है. इस बार device.createBuffer() के साथ, दूसरा जीपीयू बफ़र बनाया गया है, जो मैप नहीं की गई है. इसका इस्तेमाल वाला फ़्लैग GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ है, क्योंकि इसका इस्तेमाल पहले जीपीयू बफ़र के डेस्टिनेशन के तौर पर किया जाएगा. साथ ही, जीपीयू कॉपी निर्देश चलने के बाद, इसे 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
});

जीपीयू एक इंडिपेंडेंट को-प्रोसेसर है, इसलिए सभी जीपीयू कमांड, एसिंक्रोनस तरीके से लागू किए जाते हैं. इसलिए, जीपीयू कमांड तैयार किए जाते हैं और ज़रूरत पड़ने पर बैच में भेजे जाते हैं. WebGPU में, device.createCommandEncoder() से मिला जीपीयू कमांड एन्कोडर, एक JavaScript ऑब्जेक्ट है जो "बफ़र किए गए" निर्देशों का बैच बनाता है. इसे किसी समय जीपीयू को भेजा जाएगा. वहीं दूसरी ओर, GPUBuffer के तरीके "अनबफ़र" किए गए हैं. इसका मतलब है कि कॉल किए जाने के समय, ये अपने-आप लागू हो जाते हैं.

जीपीयू कमांड एन्कोडर मिलने के बाद, नीचे दिखाए गए तरीके के मुताबिक copyEncoder.copyBufferToBuffer() को कॉल करें और इस कमांड को कमांड लिस्ट में जोड़ें, ताकि इसे बाद में इस्तेमाल किया जा सके. आखिर में, copyEncoder.finish() को कॉल करके कोड में बदलने के निर्देशों को पूरा करें और उन्हें जीपीयू डिवाइस कमांड सूची में सबमिट करें. 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]);

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

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

यह सैंपल आज़माएं.

कम शब्दों में कहें, तो बफ़र मेमोरी की कार्रवाइयों के बारे में आपको नीचे दी गई बातें याद रखने की ज़रूरत है:

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

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

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

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

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

संक्षेप में, यहां बताया गया है कि हम क्या करने वाले हैं:

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

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

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

JavaScript में किसी मैट्रिक्स को सामान्य तौर पर दिखाना और गणित के नोटेशन में इसके बराबर होना
JavaScript में मैट्रिक्स और इसके गणितीय नोटेशन में इसके बराबर को आसानी से दिखाना

ये तीन जीपीयू बफ़र, स्टोरेज बफ़र हैं. हमें कंप्यूट शेडर में डेटा सेव और वापस पाने की ज़रूरत होती है. यह बताता है कि जीपीयू बफ़र के इस्तेमाल से जुड़े फ़्लैग में, इन सभी के लिए 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) के साथ इसकी पाइपलाइन और इसके बाइंड ग्रुप को passEncoder.setBindGroup(0, bindGroup) के साथ इंडेक्स 0 पर सेट करें. इंडेक्स 0 का मतलब है कि डब्ल्यूजीएसएल कोड में group(0) की सजावट का क्या मतलब है.

अब बात करते हैं कि यह कंप्यूट शेडर जीपीयू पर कैसे काम करेगा. हमारा लक्ष्य इस प्रोग्राम को सिलसिलेवार तरीके से, नतीजे की मैट्रिक्स की हर सेल के लिए एक साथ लागू करना है. उदाहरण के लिए, साइज़ 16 x 32 के नतीजे की मैट्रिक्स के लिए, एक्ज़ीक्यूशन के कमांड को कोड में बदलने के लिए, @workgroup_size(8, 8) पर, हम passEncoder.dispatchWorkgroups(2, 4) या passEncoder.dispatchWorkgroups(16 / 8, 32 / 8) को कॉल करेंगे. पहला तर्क "x" पहला डाइमेंशन और दूसरा डाइमेंशन "y" है. दूसरा डाइमेंशन "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 की मदद से नतीजे के मैट्रिक्स बफ़र को कॉपी करने के लिए, डेस्टिनेशन के तौर पर इस्तेमाल करने के लिए एक जीपीयू बफ़र बनाएं. आखिर में, 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() के साथ पाया जा सकता है.

मैट्रिक्स गुणा करने का नतीजा
मैट्रिक्स को गुणा करने का नतीजा

हमारे कोड में, 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 के बारे में जानने के मेरे सफ़र की शुरुआत थी. उम्मीद है कि जल्द ही ऐसे और लेखों के बारे में जानकारी मिलेगी जिनमें जीपीयू Compute में ज़्यादा जानकारी शामिल होगी. साथ ही, इस बारे में भी जानकारी मिलेगी कि WebGPU में रेंडरिंग (कैनवस, टेक्सचर, सैंपलर) कैसे काम करती है.