تستكشف هذه المشاركة واجهة برمجة التطبيقات التجريبية WebGPU من خلال أمثلة، وتساعدك في بدء إجراء عمليات حسابية متوازية للبيانات باستخدام وحدة معالجة الرسومات.
الخلفية
كما تعلم، وحدة معالجة الرسومات (GPU) هي نظام فرعي كهربائي داخل جهاز كمبيوتر كان في الأصل مخصّصًا لمعالجة الرسومات. ومع ذلك، في السنوات العشر الماضية، تطورت نحو بنية أكثر مرونة تتيح للمطوّرين تنفيذ العديد من أنواع الخوارزميات، وليس فقط عرض الرسومات الثلاثية الأبعاد، مع الاستفادة من البنية الفريدة لوحدة معالجة الرسومات. يُشار إلى هذه الإمكانات باسم "الحوسبة باستخدام وحدة معالجة الرسومات"، ويُعرف استخدام وحدة معالجة الرسومات كأحد المعالجات الملحقة للحوسبة العلمية العامة باسم برمجة وحدة معالجة الرسومات العامة (GPGPU).
ساهمت وحدات معالجة الرسومات في الحوسبة بشكل كبير في الطفرة الأخيرة في تعلُّم الآلة، لأنّ الشبكات العصبية التجميعية والنماذج الأخرى يمكنها الاستفادة من البنية لتشغيلها بكفاءة أكبر على وحدات معالجة الرسومات. بما أنّ منصة الويب الحالية لا تتضمّن إمكانات معالجة الرسومات، تعمل مجموعة "وحدة معالجة الرسومات للويب" في W3C على تصميم واجهة برمجة تطبيقات لعرض واجهات برمجة تطبيقات وحدات معالجة الرسومات الحديثة المتوفّرة على معظم الأجهزة الحالية. تُعرف واجهة برمجة التطبيقات هذه باسم WebGPU.
WebGPU هي واجهة برمجة تطبيقات من المستوى الأدنى، مثل WebGL. وهي قوية جدًا ومفصّلة للغاية، كما سيصبح واضحًا لك. لا بأس. ما نبحث عنه هو الأداء.
في هذه المقالة، سنركّز على جزء "وحدة معالجة الرسومات" من WebGPU، وسنوضّح لك أساسيات هذا الجزء فقط حتى تتمكّن من البدء في استخدامه بنفسك. سأتناول بالتفصيل ميزة 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
. بعد ذلك، يمكن استرجاع ملف تخزين البيانات الثنائية الأولية المرتبط
من خلال استدعاء طريقة ملف تخزين وحدة معالجة الرسومات 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) وسيتم قراءته في 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));
يمكنك تجربة هذا النموذج.
باختصار، إليك ما يجب تذكُّره بشأن عمليات ذاكرة التخزين المؤقت:
- يجب إلغاء ربط ذاكرة التخزين المؤقت لوحدة معالجة الرسومات لاستخدامها في إرسال بيانات الجهاز إلى "قائمة الانتظار".
- عند ربطها، يمكن قراءة وكتابة مخازن GPU باستخدام 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 لأنّنا لا نحتاج إليها هنا.
في عالم الحوسبة باستخدام وحدة معالجة الرسومات، يُعرف ترميز أمر تنفيذ دالة نواة على مجموعة من البيانات باسم الإرسال.
حجم شبكة مجموعة العمل لبرنامج shader الحسابي هو (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: [
نتائج الأداء
كيف تختلف سرعة تنفيذ عملية ضرب المصفوفات على وحدة معالجة الرسومات عن سرعتها على وحدة المعالجة المركزية؟ لمعرفة ذلك، كتبتُ البرنامج الموضّح للتو لوحدة المعالجة المركزية. وكما يمكنك الاطّلاع عليه في الرسم البياني أدناه، يبدو أنّ استخدام طاقة وحدة معالجة الرسومات الكاملة هو خيار واضح عندما يكون حجم المصفوفات أكبر من 256 x 256.
كانت هذه المقالة مجرد بداية رحلتي في استكشاف WebGPU. نتوقع قريبًا نشر المزيد من المقالات التي تتناول بالتفصيل معالجة الرسومات باستخدام وحدة معالجة الرسومات وطريقة عمل المعالجة (لوحة الرسم والنسيج وأدوات أخذ العينات) في WebGPU.