بدء استخدام GPU Compute على الويب

تستكشف هذه المشاركة واجهة برمجة تطبيقات WebGPU من خلال أمثلة وتساعدك في البدء بإجراء العمليات الحسابية المتوازية للبيانات باستخدام وحدة معالجة الرسومات.

François Beaufort
François Beaufort

الخلفية

كما تعلم، فإن وحدة معالجة الرسومات (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]);

في هذه المرحلة، يتم تعيين المخزن المؤقت لوحدة معالجة الرسومات، ما يعني أنه مملوك لوحدة المعالجة المركزية (CPU) ويمكن الوصول إليه من خلال القراءة/الكتابة من 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 على "صحيح".

برمجة Shader

تُعرف البرامج التي يتم تشغيلها على وحدة معالجة الرسومات والتي تُجري عمليات حسابية فقط (ولا ترسم مثلّثات) باسم برامج تظليل الحوسبة. ويتم تشغيلها بالتوازي بواسطة المئات من نوى وحدة معالجة الرسومات (التي أصغر حجمًا من نوى وحدة المعالجة المركزية) التي تعمل معًا لتحليل البيانات. تكون المدخلات والمخرجات مخازن مؤقتة في 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 Shader، التي يمكن ترجمتها بشكل بسيط إلى 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(). تتطلّب هذه الطريقة وسيطتَين: تنسيق مجموعة الروابط الذي أنشأناه سابقًا، ومرحلة حوسبة تحدّد نقطة دخول أداة تظليل الحوسبة (دالة WGSL main) ووحدة التحكّم في الحوسبة الفعلية التي تمّ إنشاؤها باستخدام 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 × 32، لترميز أمر التنفيذ، في @workgroup_size(8, 8)، نستدعي passEncoder.dispatchWorkgroups(2, 4) أو passEncoder.dispatchWorkgroups(16 / 8, 32 / 8). الوسيطة الأولى "x" هي البُعد الأول، والوسيطة الثانية "y" هي البُعد الثاني، والأخيرة "z" هي البُعد الثالث الذي يتم ضبطه افتراضيًا على 1، حيث لا نحتاج إليه هنا. في عالم الحوسبة الخاصة بوحدة معالجة الرسومات، يُسمى ترميز الأمر لتنفيذ دالة النواة على مجموعة من البيانات بالإرسال.

التنفيذ بالتوازي لكل خلية في مصفوفة نتائج
التنفيذ بالتوازي لكل خلية في مصفوفة نتائج

حجم شبكة مجموعة العمل الخاصة بأداة تظليل الحوسبة الخاصة بنا هو (8, 8) في رمز WGSL الخاص بنا. لهذا السبب، سيتم تقسيم "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().

نتيجة ضرب المصفوفة
نتيجة ضرب المصفوفة

في الرمز البرمجي الخاص بنا، كانت النتيجة التي تم تسجيلها في وحدة تحكُّم 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: [

نتائج الأداء

إذًا، كيف يختلف تشغيل ضرب المصفوفة على وحدة معالجة الرسومات عن تشغيله على وحدة المعالجة المركزية (CPU)؟ لمعرفة ذلك، فقد كتبت البرنامج الموصوف للتو لوحدة المعالجة المركزية. وكما ترى في الرسم البياني أدناه، يبدو استخدام القوة الكاملة لوحدة معالجة الرسومات خيارًا واضحًا عندما يكون حجم المصفوفات أكبر من 256 × 256.

مقياس أداء وحدة معالجة الرسومات مقابل وحدة المعالجة المركزية (CPU)
مقياس أداء وحدة معالجة الرسومات مقارنةً بوحدة المعالجة المركزية (CPU)

كانت هذه المقالة مجرد بداية لرحلتي لاستكشاف WebGPU. ترقَّب المزيد من المقالات التي ستتناول المزيد من التفاصيل حول الحوسبة الحسابية من خلال GPU وعن كيفية عمل العرض (اللوحة، والهيئة، والعيّنة) في WebGPU.