इस पोस्ट में, उदाहरणों की मदद से एक्सपेरिमेंट के तौर पर उपलब्ध WebGPU API के बारे में बताया गया है. साथ ही, जीपीयू का इस्तेमाल करके, डेटा-पаралल कैलकुलेशन करने में आपकी मदद की गई है.
बैकग्राउंड
आपको पता ही होगा कि ग्राफ़िक प्रोसेसिंग यूनिट (जीपीयू), कंप्यूटर में मौजूद एक इलेक्ट्रॉनिक सबसिस्टम है. इसे मूल रूप से ग्राफ़िक्स प्रोसेस करने के लिए बनाया गया था. हालांकि, पिछले 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();
दोनों फ़ंक्शन में आपको ऐसे विकल्प मिलते हैं जिनकी मदद से, अपनी पसंद के अडैप्टर (बिजली की प्राथमिकता) और डिवाइस (एक्सटेंशन, सीमाएं) के बारे में बताया जा सकता है. आसानी से समझाने के लिए, हम इस लेख में डिफ़ॉल्ट विकल्पों का इस्तेमाल करेंगे.
बफ़र मेमोरी में लिखना
हम जीपीयू के लिए मेमोरी में डेटा लिखने के लिए, JavaScript का इस्तेमाल करने का तरीका देखते हैं. यह प्रोसेस आसान नहीं है, क्योंकि आधुनिक वेब ब्राउज़र में सैंडबॉक्सिंग मॉडल का इस्तेमाल किया जाता है.
नीचे दिए गए उदाहरण में, जीपीयू से ऐक्सेस की जा सकने वाली बफ़र मेमोरी में चार बाइट लिखने का तरीका बताया गया है. यह device.createBuffer()
को कॉल करता है, जो बफ़र का साइज़ और उसका इस्तेमाल तय करता है. भले ही, इस कॉल के लिए इस्तेमाल के फ़्लैग GPUBufferUsage.MAP_WRITE
की ज़रूरत नहीं है, लेकिन हम साफ़ तौर पर बताना चाहते हैं कि हमें इस बफ़र में लिखना है. इससे, जीपीयू बफ़र ऑब्जेक्ट, बनने के समय मैप हो जाता है. हालांकि, 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]);
इस समय, जीपीयू बफ़र को मैप किया जाता है. इसका मतलब है कि इसका मालिकाना हक सीपीयू के पास होता है और इसे 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 बफ़र को JavaScript में पढ़ा और लिखा जा सकता है.
mapAsync()
औरcreateBuffer()
कोmappedAtCreation
के 'सही है' पर सेट होने पर कॉल करने पर, जीपीयू बफ़र मैप किए जाते हैं.
शेडर प्रोग्रामिंग
जीपीयू पर चलने वाले ऐसे प्रोग्राम जो सिर्फ़ कंप्यूटेशन करते हैं (और ट्रायएंगल नहीं बनाते) कंप्यूट शेडर कहलाते हैं. उन्हें सैंकड़ों ऐसे जीपीयू कोर के साथ-साथ एक्ज़ीक्यूट किया जाता है जो सीपीयू कोर से छोटे होते हैं. ये कोर, डेटा क्रंच करने के लिए एक साथ काम करते हैं. WebGPU में, इनका इनपुट और आउटपुट बफ़र होता है.
WebGPU में कंप्यूट शेडर के इस्तेमाल के उदाहरण के लिए, हम मैट्रिक्स मल्टीप्लिकेशन के साथ काम करेंगे. यह मशीन लर्निंग में इस्तेमाल होने वाला एक सामान्य एल्गोरिदम है, जिसका उदाहरण नीचे दिया गया है.
कम शब्दों में, हम ये काम करने जा रहे हैं:
- तीन 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 Shader Language की मदद से लिखा जाता है, जिसका 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 में रेंडरिंग (कैनवस, टेक्स्चर, सैंपलर) के काम करने के तरीके के बारे में ज़्यादा जानकारी देने वाले लेख उपलब्ध होंगे.