इस पोस्ट में, उदाहरणों की मदद से एक्सपेरिमेंट के तौर पर उपलब्ध WebGPU API के बारे में बताया गया है. साथ ही, जीपीयू का इस्तेमाल करके, डेटा-पаралल कैलकुलेशन करने में आपकी मदद की गई है.
बैकग्राउंड
आपको पता ही होगा कि ग्राफ़िक प्रोसेसिंग यूनिट (जीपीयू), कंप्यूटर में मौजूद एक इलेक्ट्रॉनिक सबसिस्टम है. इसे मूल रूप से ग्राफ़िक्स प्रोसेस करने के लिए बनाया गया था. हालांकि, पिछले 10 सालों में, यह ज़्यादा फ़्लेक्सिबल आर्किटेक्चर में बदल गया है. इससे डेवलपर, जीपीयू के यूनीक आर्किटेक्चर का फ़ायदा लेते हुए, 3D ग्राफ़िक्स को रेंडर करने के साथ-साथ कई तरह के एल्गोरिदम लागू कर सकते हैं. इन क्षमताओं को जीपीयू कंप्यूट कहा जाता है. अलग-अलग कामों के लिए इस्तेमाल की जाने वाली साइंटिफ़िक कंप्यूटिंग के लिए, जीपीयू को को-प्रोसेसर के तौर पर इस्तेमाल करने को सामान्य-उद्देश्य जीपीयू (जीपीजीपीयू) प्रोग्रामिंग कहा जाता है.
जीपीयू कंप्यूट ने हाल ही में मशीन लर्निंग के क्षेत्र में हुए उछाल में काफ़ी योगदान दिया है. ऐसा इसलिए, क्योंकि कन्वोल्यूशन न्यूरल नेटवर्क और अन्य मॉडल, जीपीयू पर ज़्यादा बेहतर तरीके से काम करने के लिए, आर्किटेक्चर का फ़ायदा ले सकते हैं. मौजूदा Web Platform में जीपीयू कंप्यूट की सुविधाएं नहीं हैं. इसलिए, W3C का "वेब के लिए GPU" कम्यूनिटी ग्रुप एक एपीआई डिज़ाइन कर रहा है, ताकि ऐसे मॉडर्न जीपीयू एपीआई दिखाए जा सकें जो ज़्यादातर मौजूदा डिवाइसों पर उपलब्ध हैं. इस एपीआई को WebGPU कहा जाता है.
WebGPU, WebGL की तरह ही एक लो-लेवल एपीआई है. यह बहुत ज़्यादा जानकारी देने वाला और बेहतरीन तरीका है, जैसा कि आपको दिखेगा. लेकिन कोई बात नहीं. हम परफ़ॉर्मेंस पर ध्यान देते हैं.
इस लेख में, मैं WebGPU के GPU Compute वाले हिस्से पर फ़ोकस करूंगा. सच कहूं, तो मैं सिर्फ़ इस बारे में बता रहा हूं, ताकि आप खुद इस बारे में जान सकें. आने वाले लेखों में, मैं WebGPU रेंडरिंग (कैनवस, टेक्स्चर वगैरह) के बारे में ज़्यादा जानकारी दूंगा.
जीपीयू को ऐक्सेस करना
WebGPU में जीपीयू को ऐक्सेस करना आसान है. navigator.gpu.requestAdapter()
को कॉल करने पर, JavaScript प्रॉमिस का रिस्पॉन्स मिलता है. यह जीपीयू अडैप्टर के साथ एसिंक्रोनस तरीके से रिज़ॉल्व हो जाता है. इस अडैप्टर को ग्राफ़िक्स कार्ड के तौर पर देखें. यह इंटिग्रेट किया गया (सीपीयू के साथ एक ही चिप पर) या डिस्क्रेट (आम तौर पर, 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()
को कॉल करना.
मैप किए गए/अनमैप किए गए कॉन्सेप्ट की ज़रूरत, रेस कंडीशन से बचने के लिए होती है. रेस कंडीशन में, जीपीयू और सीपीयू एक ही समय पर मेमोरी को ऐक्सेस करते हैं.
बफ़र मेमोरी पढ़ें
आइए, अब देखते हैं कि जीपीयू बफ़र को किसी दूसरे जीपीयू बफ़र में कॉपी कैसे करें और फिर इसे पढ़ें.
हम पहले जीपीयू बफ़र में लिख रहे हैं और हमें इसे दूसरे जीपीयू बफ़र में कॉपी करना है. इसलिए, इस्तेमाल से जुड़ा नया फ़्लैग GPUBufferUsage.COPY_SRC
ज़रूरी है. दूसरा जीपीयू बफ़र, इस बार मैप नहीं किए गए स्टेटस में बनाया गया है. इसके लिए, 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
});
जीपीयू एक इंडिपेंडेंट को-प्रोसेसर है, इसलिए जीपीयू के सभी निर्देश एसिंक्रोनस तरीके से एक्ज़ीक्यूट किए जाते हैं. इसलिए, जीपीयू के निर्देशों की एक सूची बनाई जाती है और ज़रूरत पड़ने पर, उन्हें एक साथ भेजा जाता है. 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 में पढ़ा और लिखा जा सकता है.
- जीपीयू बफ़र तब मैप किए जाते हैं, जब
mappedAtCreation
वालेmapAsync()
औरcreateBuffer()
को 'सही है' पर सेट किया जाता है.
शेडर प्रोग्रामिंग
जीपीयू पर चलने वाले ऐसे प्रोग्राम जिन्हें सिर्फ़ कैलकुलेशन करने के लिए इस्तेमाल किया जाता है और जो ट्राइऐंगल नहीं बनाते उन्हें कंप्यूट शेडर कहा जाता है. ये निर्देश, सैकड़ों जीपीयू कोर (जो सीपीयू कोर से छोटे होते हैं) के साथ-साथ लागू किए जाते हैं. ये कोर, डेटा को प्रोसेस करने के लिए एक साथ काम करते हैं. WebGPU में, इनका इनपुट और आउटपुट बफ़र होता है.
WebGPU में कंप्यूट शेडर के इस्तेमाल को दिखाने के लिए, हम मैट्रिक्स गुणा करने की सुविधा का इस्तेमाल करेंगे. यह मशीन लर्निंग में इस्तेमाल होने वाला एक सामान्य एल्गोरिदम है. इसका उदाहरण नीचे दिया गया है.
संक्षेप में, यहां बताया गया है कि हम क्या करने जा रहे हैं:
- तीन GPU बफ़र बनाएं (दो मैट्रिक्स के लिए, जिनका गुणा करना है और एक नतीजे वाली मैट्रिक के लिए)
- कंप्यूट शेडर के इनपुट और आउटपुट के बारे में बताना
- कंप्यूट शेडर कोड को कंपाइल करना
- कंप्यूट पाइपलाइन सेट अप करना
- कोड में बदले गए निर्देशों को जीपीयू पर एक साथ सबमिट करना
- नतीजे के मैट्रिक्स जीपीयू बफ़र को पढ़ना
जीपीयू बफ़र बनाना
आसानी के लिए, मैट्रिक्स को फ़्लोटिंग पॉइंट नंबर की सूची के तौर पर दिखाया जाएगा. पहला एलिमेंट, पंक्तियों की संख्या होता है, दूसरा एलिमेंट कॉलम की संख्या होता है, और बाकी एलिमेंट मैट्रिक्स की असल संख्याएं होती हैं.
तीनों जीपीयू बफ़र, स्टोरेज बफ़र होते हैं, क्योंकि हमें कंप्यूट शेडर में डेटा को स्टोर और फिर से पाना होता है. इससे पता चलता है कि 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]);
नतीजे की मैट्रिक्स पढ़ना
नतीजे के मैट्रिक्स को पढ़ना, 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 के बारे में ज़्यादा जानने के मेरे सफ़र की शुरुआत भर है. जल्द ही, जीपीयू कंप्यूट और WebGPU में रेंडरिंग (कैनवस, टेक्स्चर, सैंपलर) के काम करने के तरीके के बारे में ज़्यादा जानकारी देने वाले लेख उपलब्ध होंगे.