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

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

François Beaufort
François Beaufort

الخلفية

وكما تعلم بالفعل، فإن وحدة معالجة الرسومات (GPU) هي وحدة معالجة نظام فرعي داخل جهاز كمبيوتر كان متخصصًا في الأصل للمعالجة والرسومات. ومع ذلك، تطورت في السنوات العشر الماضية نحو الحصول على هندسة معمارية تسمح للمطورين بتنفيذ العديد من أنواع الخوارزميات، وليس فقط مع عرض رسومات ثلاثية الأبعاد، مع الاستفادة من البنية الفريدة وحدة معالجة رسومات. ويُشار إلى هذه الإمكانات باسم "حوسبة GPU"، كما أن استخدام "وحدة معالجة الرسومات" يُسمى المعالج المساعد للحوسبة العلمية للأغراض العامة برمجة وحدة معالجة الرسومات (GPGPU).

ساهمت الحوسبة الخاصة بوحدة معالجة الرسومات بشكل كبير في ازدهار تعلّم الآلة في الآونة الأخيرة، حيث يمكن للشبكات العصبية الالتفافية والنماذج الأخرى الاستفادة من البنية التشغيلية بكفاءة أكبر باستخدام وحدات معالجة الرسومات. مع منصة الويب الحالية إلى إمكانات حوسبة GPU، فإن "وحدة معالجة الرسومات للويب" التابعة لـ W3C مجموعة المنتدى تصمم واجهة برمجة تطبيقات للكشف عن واجهات برمجة التطبيقات الحديثة لوحدة معالجة الرسومات المتوفرة على الأجهزة الحالية. ويُطلق على واجهة برمجة التطبيقات هذه اسم WebGPU.

WebGPU هي واجهة برمجة تطبيقات منخفضة المستوى، مثل WebGL. إنها قوية جدًا ومطولة للغاية، التي ستراه. لكن لا بأس. ما نبحث عنه هو الأداء.

سوف أركز في هذه المقالة على جزء حوسبة وحدة معالجة الرسومات في WebGPU، في الحقيقة، كل ما في الأمر أنني أتناولها قليلاً حتى يمكنك بدء اللعب على تمتلكه. سأتعمق أكثر وأتناول عرض WebGPU (اللوحة والزخرفة وما إلى ذلك) في المقالات القادمة.

الوصول إلى وحدة معالجة الرسومات

يمكن الوصول إلى وحدة معالجة الرسومات بسهولة من خلال WebGPU. جارٍ الاتصال بالرقم navigator.gpu.requestAdapter() عرض وعد JavaScript يمكن حله بشكل غير متزامن مع وحدة معالجة الرسومات محوّل. فكر في هذا المحول على أنه بطاقة الرسومات. ويمكن أن تتكامل (على الشريحة نفسها التي بها وحدة المعالجة المركزية (CPU)) أو منفصلة (عادةً ما تكون بطاقة PCIe أكبر أداءً ولكنه يستخدم المزيد من الطاقة).

بعد الانتهاء من توفير محوّل وحدة معالجة الرسومات، يُرجى الاتصال بالرقم adapter.requestDevice() للحصول على وعد. سيتم حلها باستخدام جهاز وحدة معالجة رسومات ستستخدمه لإجراء بعض العمليات الحسابية على وحدة معالجة الرسومات.

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();

تستخدم كلتا الدالتين خيارات تتيح لك أن تكون محددًا بشأن نوع محوّل (تفضيل الطاقة) والجهاز (الإضافات والحدود) التي تريدها. بالنسبة إلى ببساطة، سنستخدم الخيارات التلقائية في هذه المقالة.

كتابة ذاكرة التخزين المؤقت

لنتعرّف على كيفية استخدام JavaScript لكتابة البيانات إلى الذاكرة لوحدة GPU. هذا النمط عملية غير مباشرة بسبب نموذج وضع الحماية المستخدم في تطبيقات الويب المتصفحات.

يوضح المثال أدناه كيفية كتابة أربع بايت للتخزين المؤقت للذاكرة التي يمكن الوصول إليها من وحدة معالجة الرسومات. وتستدعي 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)، يمكن الوصول إليه بالقراءة/الكتابة من JavaScript. لكي تتمكن وحدة معالجة الرسومات من الوصول إليه، غير معيّن، وهو ما لا يقل عن سهولة الاتصال بـ gpuBuffer.unmap().

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

قراءة الذاكرة المؤقتة

لنرى الآن كيفية نسخ المخزن المؤقت لوحدة معالجة الرسومات إلى مخزن تخزين مؤقت آخر لوحدة معالجة الرسومات وقراءته مرة أخرى.

نظرًا لأننا نكتب في المخزن المؤقت الأول لوحدة معالجة الرسومات ونريد نسخه إلى وحدة معالجة الرسومات المؤقتة، يجب توفُّر علامة استخدام جديدة للسمة 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

البرامج التي تعمل على وحدة معالجة الرسومات (GPU) والتي تجري عمليات حسابية فقط (ولا ترسم المثلثات) تسمى أدوات تظليل الحوسبة. يتم تنفيذ هذه الإجراءات بالتوازي من قِبل المئات. من نوى وحدة معالجة الرسومات (التي تكون أصغر من نوى وحدة المعالجة المركزية) التي تعمل معًا للمعالجة البيانات. إدخالاتها ومخرجاتها هي مخازن مؤقتة في WebGPU.

لتوضيح استخدام أدوات تظليل الحوسبة في WebGPU، سنلعب باستخدام مصفوفة الضرب، وهي خوارزمية شائعة في التعلم الآلي يتم توضيحها أدناه.

مخطط ضرب المصفوفة
مخطط ضرب المصفوفة

باختصار، إليك ما سنقوم به:

  1. أنشئ ثلاثة مخازن GPU (وهما مصفوفات لضرب المصفوفات وواحدًا مصفوفة النتائج)
  2. وصف الإدخال والإخراج لأداة تظليل الحوسبة
  3. تجميع رمز أداة تظليل Compute
  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 لأداة تظليل الحوسبة من ناحية أخرى، تربط مجموعة الربط، المحددة لتخطيط مجموعة الربط هذا، المخزن المؤقت لوحدة 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، والتي يمكن ترجمتها بشكل بسيط إلى 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"
  }
});

إرسال الأوامر

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

لنبدأ تشغيل برنامج ترميز قابل للبرمجة Compute Pass 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();

لإيقاف برنامج ترميز Compute Pass، يُرجى الاتصال بالرقم 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 وانتظار حلّ الوعد السابق الذي يشير إلى المخزن المؤقت لوحدة GPU. في هذه المرحلة، من الممكن الحصول على خريطة النطاق مع 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.