يستكشف هذا المنشور واجهة برمجة التطبيقات WebGPU API من خلال أمثلة، ويساعدك في بدء إجراء عمليات حسابية موازية للبيانات باستخدام وحدة معالجة الرسومات.
الخلفية
كما تعلم، وحدة معالجة الرسومات (GPU) هي نظام فرعي كهربائي داخل جهاز كمبيوتر كان في الأصل مخصّصًا لمعالجة الرسومات. ومع ذلك، في السنوات العشر الماضية، تطورت نحو بنية أكثر مرونة تتيح للمطوّرين تنفيذ العديد من أنواع الخوارزميات، وليس فقط عرض الرسومات الثلاثية الأبعاد، مع الاستفادة من البنية الفريدة لوحدة معالجة الرسومات. يُشار إلى هذه الإمكانات باسم "الحوسبة باستخدام وحدة معالجة الرسومات"، ويُعرف استخدام وحدة معالجة الرسومات كأحد المعالجات الملحقة للحوسبة العلمية العامة باسم برمجة وحدة معالجة الرسومات العامة (GPGPU).
ساهمت وحدات معالجة الرسومات في الحوسبة بشكل كبير في الطفرة الأخيرة في تعلُّم الآلة، لأنّ الشبكات العصبية التجميعية والنماذج الأخرى يمكنها الاستفادة من البنية لتشغيلها بكفاءة أكبر على وحدات معالجة الرسومات. بما أنّ منصة الويب الحالية لا تتضمّن إمكانات حوسبة وحدة معالجة الرسومات، تعمل مجموعة "وحدة معالجة الرسومات للويب" في W3C على تصميم واجهة برمجة تطبيقات لعرض واجهات برمجة تطبيقات وحدة معالجة الرسومات الحديثة المتوفّرة على معظم الأجهزة الحالية. ويُطلق على واجهة برمجة التطبيقات هذه اسم WebGPU.
WebGPU هي واجهة برمجة تطبيقات من المستوى الأدنى، مثل WebGL. وهي قوية جدًا ومفصّلة للغاية، كما ستلاحظ. لا بأس. ما نبحث عنه هو الأداء.
في هذه المقالة، سأركز على جزء "حوسبة وحدة معالجة الرسومات" 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]);
في هذه المرحلة، يتم تعيين المخزن المؤقت لوحدة GPU، مما يعني أنه ملك لوحدة المعالجة المركزية (CPU)، ويمكن الوصول إليه بالقراءة/الكتابة من جافا سكريبت. لكي تتمكّن وحدة معالجة الرسومات من الوصول إلى المساحة، يجب أولاً
إلغاء ربطها، وذلك من خلال استدعاء gpuBuffer.unmap()
.
هناك حاجة إلى مفهوم الربط/عدم التعيين لمنع حالات السباق التي تصل فيها وحدة معالجة الرسومات ووحدة المعالجة المركزية إلى الذاكرة في الوقت نفسه.
ذاكرة التخزين المؤقت للقراءة
لنرى الآن كيفية نسخ المخزن المؤقت لوحدة معالجة الرسومات إلى مخزن تخزين مؤقت آخر لوحدة معالجة الرسومات وقراءته مرة أخرى.
بما أنّنا نكتب في أول وحدة تخزين مؤقت لوحدة معالجة الرسومات ونريد نسخها إلى ملف تخزين مؤقت
ثاني لوحدة معالجة الرسومات، يجب استخدام علامة استخدام جديدة GPUBufferUsage.COPY_SRC
. يتم إنشاء المخزن المؤقت الثاني لوحدة معالجة الرسومات
في حالة غير معيّنة هذه المرة باستخدام
device.createBuffer()
. علامة الاستخدام هي GPUBufferUsage.COPY_DST |
GPUBufferUsage.MAP_READ
لأنّه سيتم استخدامه كوجهة لأول ملف ذاكرة مؤقتة لوحدة معالجة الرسومات (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()
وإرسالها
إلى قائمة انتظار أوامر جهاز وحدة معالجة الرسومات. تتحمّل "القائمة الانتظار" مسؤولية معالجة
عمليات الإرسال التي تتم من خلال 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.
- يتم ربط مخازن GPU عند استدعاء
mapAsync()
وcreateBuffer()
مع تحديدmappedAtCreation
على true.
برمجة Shader
تسمى البرامج التي تعمل على وحدة معالجة الرسومات التي تجري عمليات حسابية فقط (ولا ترسم مثلات مثلثات) بأدوات تظليل الحوسبة. ويتم تنفيذها بشكل موازٍ من خلال مئات نوى وحدة معالجة الرسومات (التي تكون أصغر من نوى وحدة المعالجة المركزية) التي تعمل معًا لمعالجة البيانات. وتكون الإدخالات والمخرجات عبارة عن مخازن مؤقتة في WebGPU.
لتوضيح استخدام برامج shaders الحسابية في WebGPU، سنستخدم عملية ضرب المصفوفات، وهي خوارزمية شائعة في تعلُّم الآلة موضّحة أدناه.
باختصار، في ما يلي الإجراءات التي سننفذها:
- أنشئ ثلاثة مخازن لوحدة معالجة الرسومات (اثنان للمصفوفات التي سيتم ضربها وواحد ل مصفوفة النتائج).
- وصف الإدخال والإخراج لشريحة البرامج الحسابية
- تجميع رمز برنامج التظليل الحسابي
- إعداد مسار بيانات حسابية
- إرسال الأوامر المشفَّرة إلى وحدة معالجة الرسومات بشكل مجمّع
- قراءة المخزن المؤقت لوحدة معالجة الرسومات في مصفوفة النتائج
إنشاء المخازن المؤقتة لوحدة معالجة الرسومات
ولتبسيط الأمر، سيتم تمثيل المصفوفات كقائمة بأرقام نقطة تشكلة. العنصر الأول هو عدد الصفوف، والعنصر الثاني هو عدد الأعمدة، والباقي هو الأرقام الفعلية للمصفوفة.
وتكون مصفوفات التخزين الثلاث لوحدة معالجة الرسومات هي مصفوفات التخزين لأنّنا نحتاج إلى تخزين البيانات واستردادها في
shader الحسابي. يوضّح هذا سبب احتواء علامات استخدام المخزن المؤقت لوحدة معالجة الرسومات
على 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
لبرنامج التظليل الحسابي.
من ناحية أخرى، تربط مجموعة الربط، التي تم تحديدها لتنسيق مجموعة الربط هذا،
مخازن GPU بالعناصر: 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 مع ديكور
group(0)
في رمز WGSL.
والآن، لنتحدث عن كيفية تشغيل أداة تظليل الحوسبة هذه على وحدة معالجة الرسومات. هدفنا هو تنفيذ هذا البرنامج بالتوازي لكل خلية من مصفوفة النتائج،
خطوة بخطوة. بالنسبة إلى مصفوفة النتائج التي يبلغ حجمها 16 x 32 مثلاً، لتشفير
أمر التنفيذ، في @workgroup_size(8, 8)
، سنستدعي
passEncoder.dispatchWorkgroups(2, 4)
أو passEncoder.dispatchWorkgroups(16 / 8, 32 / 8)
.
الوسيطة الأولى "x" هي السمة الأولى، والوسيطة الثانية "y" هي السمة الثانية،
والوسيطة الأخيرة "z" هي السمة الثالثة التي تكون قيمتها التلقائية 1 لأنّنا لا نحتاج إليها هنا.
في عالم الحوسبة باستخدام وحدة معالجة الرسومات، يُعرف ترميز أمر تنفيذ دالة نواة على مجموعة من البيانات باسم الإرسال.
حجم شبكة مجموعة العمل لبرنامجنا المشتمِل على تظليل حسابي هو (8, 8)
في رمز WGSL. ولهذا السبب، سيتم تقسيم "س" و "ص"، وهما عدد صفوف
المصفوفة الأولى وعدد أعمدة المصفوفة الثانية على التوالي،
بـ 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()
. بعد ذلك، أنشئ ملف تدوير ذاكرة GPU لاستخدامه كوجهة لنسخ ملف تدوير ذاكرة مصفوفة النتائج باستخدام 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()
.
في الرمز البرمجي، النتيجة المسجّلة في وحدة تحكّم JavaScript في DevTools هي "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: [
نتائج الأداء
إذًا، كيف يقارن تشغيل عملية ضرب المصفوفة على وحدة معالجة رسومات بتشغيلها على وحدة المعالجة المركزية؟ لمعرفة ذلك، كتبت البرنامج الموصوف للتوّ لوحدة المعالجة المركزية. وكما ترى في الرسم البياني أدناه، يبدو أنّ استخدام القوة الكاملة لوحدة GPU خيار واضح، وذلك عندما يكون حجم المصفوفات أكبر من 256 × 256.
كانت هذه المقالة مجرد بداية رحلتي في استكشاف WebGPU. نتوقع قريبًا نشر المزيد من المقالات التي تتناول بالتفصيل معالجة الرسومات باستخدام وحدة معالجة الرسومات وطريقة عمل المعالجة (لوحة الرسم والنسيج وأدوات أخذ العينات) في WebGPU.