เริ่มต้นใช้งาน GPU Compute บนเว็บ

โพสต์นี้จะสำรวจ WebGPU API เวอร์ชันทดลองผ่านตัวอย่างและช่วยให้คุณเริ่มต้นใช้งานการประมวลผลแบบขนานกันโดยใช้ GPU

François Beaufort
François Beaufort

ข้อมูลเบื้องต้น

อย่างที่คุณอาจทราบแล้ว หน่วยประมวลผลกราฟิก (GPU) คือระบบย่อยอิเล็กทรอนิกส์ภายในคอมพิวเตอร์ที่เดิมทีออกแบบมาเพื่อประมวลผลกราฟิกโดยเฉพาะ อย่างไรก็ตาม ในช่วง 10 ปีที่ผ่านมา สถาปัตยกรรมของ GPU ได้พัฒนาไปให้มีความยืดหยุ่นมากขึ้น ซึ่งช่วยให้นักพัฒนาซอฟต์แวร์สามารถใช้อัลกอริทึมได้หลายประเภท ไม่ใช่แค่แสดงผลกราฟิก 3 มิติเท่านั้น และยังใช้ประโยชน์จากสถาปัตยกรรมเฉพาะของ GPU ได้อีกด้วย ความสามารถเหล่านี้เรียกว่า GPU Compute และการใช้ GPU เป็นหน่วยประมวลผลร่วมสำหรับการประมวลผลทางวิทยาศาสตร์ทั่วไปเรียกว่าการเขียนโปรแกรม GPU (GPGPU) ทั่วไป

การคำนวณด้วย GPU มีส่วนสำคัญอย่างยิ่งในการเติบโตของแมชชีนเลิร์นนิงในช่วงที่ผ่านมา เนื่องจากโครงข่ายประสาทแบบ Convolution และโมเดลอื่นๆ ใช้ประโยชน์จากสถาปัตยกรรมนี้เพื่อทำงานได้อย่างมีประสิทธิภาพมากขึ้นใน GPU เนื่องจากแพลตฟอร์มเว็บในปัจจุบันไม่มีความสามารถในการประมวลผลด้วย GPU กลุ่มชุมชน "GPU สําหรับเว็บ" ของ W3C จึงออกแบบ API เพื่อแสดง GPU API สมัยใหม่ที่มีให้บริการในอุปกรณ์ส่วนใหญ่ในปัจจุบัน API นี้มีชื่อว่า WebGPU

WebGPU เป็น API ระดับล่าง เช่น WebGL ข้อมูลนี้ทรงพลังและมีรายละเอียดเยอะ อย่างที่คุณเห็น แต่ไม่เป็นไร สิ่งที่เรามองหาคือประสิทธิภาพ

ในบทความนี้ เราจะมุ่งเน้นที่ส่วน GPU Compute ของ WebGPU และขอบอกตามตรงว่าเราเพิ่งจะเริ่มต้นเท่านั้น เพื่อให้คุณเริ่มเล่นด้วยตนเองได้ เราจะเจาะลึกและอธิบายการเรนเดอร์ WebGPU (Canvas, พื้นผิว และอื่นๆ) ในบทความที่กำลังจะเผยแพร่

เข้าถึง GPU

การเข้าถึง GPU ใน WebGPU ทำได้ง่าย การเรียกใช้ navigator.gpu.requestAdapter() จะแสดงสัญญา JavaScript ที่จะแปลงข้อมูลแบบไม่พร้อมกันด้วยอะแดปเตอร์ GPU ให้คิดว่าอะแดปเตอร์นี้เป็นการ์ดกราฟิก ซึ่งอาจผสานรวม (บนชิปเดียวกันกับ CPU) หรือแยกกันก็ได้ (โดยปกติจะเป็นการ์ด PCI ที่มีประสิทธิภาพมากกว่าแต่ใช้พลังงานมากกว่า)

เมื่อคุณมีแอปแดปเตอร์ GPU แล้ว ให้เรียกใช้ adapter.requestDevice() เพื่อรับสัญญาว่าจะดำเนินการซึ่งจะแสดงผลเป็นอุปกรณ์ GPU ที่คุณจะใช้ทำการคำนวณบางอย่างของ GPU

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

ฟังก์ชันทั้ง 2 ฟังก์ชันใช้ตัวเลือกที่ให้คุณระบุประเภทของอะแดปเตอร์ (ค่ากำหนดกำลังไฟฟ้า) และอุปกรณ์ (ส่วนขยาย ข้อจำกัด) ที่ต้องการได้ เพื่อให้เข้าใจง่าย เราจะใช้ตัวเลือกเริ่มต้นในบทความนี้

หน่วยความจำบัฟเฟอร์การเขียน

มาดูวิธีใช้ JavaScript เพื่อเขียนข้อมูลลงในหน่วยความจําสําหรับ GPU กระบวนการนี้ไม่ตรงไปตรงมาเนื่องจากรูปแบบแซนด์บ็อกซ์ที่ใช้ในเว็บเบราว์เซอร์สมัยใหม่

ตัวอย่างด้านล่างแสดงวิธีเขียน 4 ไบต์ไปยังบัฟเฟอร์หน่วยความจำที่เข้าถึงได้จาก GPU ซึ่งจะเรียก device.createBuffer() ซึ่งจะรับขนาดบัฟเฟอร์และการใช้งาน แม้ว่าการเรียกใช้นี้ไม่จำเป็นต้องใช้ Flag การใช้งาน GPUBufferUsage.MAP_WRITE แต่เราต้องการเขียนลงในบัฟเฟอร์นี้อย่างชัดเจน ซึ่งส่งผลให้ออบเจ็กต์บัฟเฟอร์ GPU ที่แมปขณะสร้างเนื่องจากการตั้งค่า mappedAtCreation เป็น "จริง" จากนั้นเรียกข้อมูลบัฟเฟอร์ข้อมูลไบนารีดิบที่เกี่ยวข้องได้โดยเรียกใช้เมธอดบัฟเฟอร์ 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]);

ณ จุดนี้ ระบบจะแมปบัฟเฟอร์ GPU ซึ่งหมายความว่า CPU เป็นเจ้าของและเข้าถึงได้ในรูปแบบการอ่าน/เขียนจาก JavaScript คุณต้องยกเลิกการแมปเพื่อให้ GPU เข้าถึงได้ ซึ่งทำได้ง่ายๆ เพียงเรียกใช้ gpuBuffer.unmap()

แนวคิดของการแมป/ยกเลิกการแมปจําเป็นเพื่อป้องกันเงื่อนไขการแข่งขันที่ GPU และ CPU เข้าถึงหน่วยความจําพร้อมกัน

อ่านหน่วยความจำบัฟเฟอร์

มาดูวิธีคัดลอกบัฟเฟอร์ GPU ไปยังบัฟเฟอร์ GPU อื่นและอ่านกลับกัน

เนื่องจากเราเขียนลงในบัฟเฟอร์ GPU แรกและต้องการคัดลอกไปยังบัฟเฟอร์ GPU ที่ 2 จึงต้องใช้ Flag การใช้งานใหม่ GPUBufferUsage.COPY_SRC ระบบจะสร้างบัฟเฟอร์ GPU ตัวที่ 2 ในสถานะไม่ได้แมปในครั้งนี้ด้วย device.createBuffer() Flag การใช้งานคือ GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ เนื่องจากจะใช้เป็นปลายทางของบัฟเฟอร์ GPU แรกและอ่านใน JavaScript เมื่อมีการเรียกใช้คำสั่งการคัดลอก GPU

// 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
});

เนื่องจาก GPU เป็นหน่วยประมวลผลร่วมอิสระ ระบบจึงจะเรียกใช้คําสั่ง GPU ทั้งหมดแบบไม่พร้อมกัน ด้วยเหตุนี้ จึงมีรายการคำสั่ง GPU ที่สร้างและส่งเป็นกลุ่มเมื่อจำเป็น ใน WebGPU ตัวแปลงรหัสคำสั่ง GPU ที่ device.createCommandEncoder() แสดงผลเป็นออบเจ็กต์ JavaScript ที่สร้างชุดคำสั่ง "บัฟเฟอร์" ที่จะส่งไปยัง GPU ณ เวลาหนึ่งๆ ในทางกลับกัน เมธอดใน GPUBuffer จะเป็น "ไม่มีการบัฟเฟอร์" ซึ่งหมายความว่าจะดำเนินการแบบอะตอมในขณะที่มีการเรียกใช้

เมื่อคุณมีโปรแกรมเข้ารหัสคําสั่ง GPU แล้ว ให้เรียกใช้ copyEncoder.copyBufferToBuffer() ตามที่แสดงด้านล่างเพื่อเพิ่มคําสั่งนี้ลงในคิวคําสั่งสําหรับการดําเนินการในภายหลัง สุดท้าย ใช้คำสั่งการเข้ารหัสให้เสร็จโดยเรียกใช้ copyEncoder.finish() และส่งคำสั่งเหล่านั้นไปยังคิวคำสั่งของอุปกรณ์ GPU คิวมีหน้าที่จัดการการส่งผ่าน device.queue.submit() ที่มีคำสั่ง GPU เป็นอาร์กิวเมนต์ ซึ่งจะดำเนินการคำสั่งทั้งหมดที่จัดเก็บไว้ในอาร์เรย์ตามลำดับ

// 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 แล้ว แต่ไม่จําเป็นต้องดําเนินการ หากต้องการอ่านบัฟเฟอร์ GPU ที่ 2 ให้เรียกใช้ gpuReadBuffer.mapAsync() ด้วย GPUMapMode.READ โดยจะแสดงสัญญาที่จะได้รับการแก้ไขเมื่อมีการแมปบัฟเฟอร์ GPU จากนั้นรับช่วงที่แมปด้วย gpuReadBuffer.getMappedRange() ซึ่งมีค่าเดียวกับบัฟเฟอร์ GPU แรกเมื่อมีการเรียกใช้คําสั่ง GPU ที่รอดำเนินการทั้งหมดแล้ว

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const copyArrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Uint8Array(copyArrayBuffer));

คุณสามารถลองใช้ตัวอย่างนี้

กล่าวโดยย่อ สิ่งที่คุณต้องจำเกี่ยวกับการดำเนินการของหน่วยความจำบัฟเฟอร์มีดังนี้

  • คุณต้องยกเลิกการแมปบัฟเฟอร์ GPU เพื่อนำไปใช้ในการส่งคิวอุปกรณ์
  • เมื่อแมปแล้ว คุณจะอ่านและเขียนบัฟเฟอร์ GPU ใน JavaScript ได้
  • ระบบจะแมปบัฟเฟอร์ GPU เมื่อมีการเรียกใช้ mapAsync() และ createBuffer() ที่มี mappedAtCreation ที่ตั้งค่าเป็น "จริง"

โปรแกรมการเขียนเฉดสี

โปรแกรมที่ทำงานบน GPU ที่ประมวลผลเพียงการคำนวณเท่านั้น (และไม่วาดรูปสามเหลี่ยม) เรียกว่า "ตัวปรับแสงเงาประมวลผล" โดยระบบจะประมวลผลไปพร้อมๆ กันด้วยแกน GPU หลายร้อยแกน (ซึ่งเล็กกว่าแกน CPU) ที่ทำงานร่วมกันเพื่อประมวลผลข้อมูล อินพุตและเอาต์พุตของ WebGPU คือบัฟเฟอร์

เราจะอธิบายการใช้เชดเดอร์การประมวลผลใน WebGPU โดยใช้การคูณเมทริกซ์ ซึ่งเป็นอัลกอริทึมทั่วไปในแมชชีนเลิร์นนิงดังที่แสดงด้านล่าง

แผนภาพการคูณเมทริกซ์
แผนภาพการคูณเมตริกซ์

โดยสรุปแล้ว สิ่งที่เราจะทำมีดังนี้

  1. สร้างบัฟเฟอร์ GPU 3 รายการ (2 รายการสำหรับเมทริกซ์ที่จะคูณ และ 1 รายการสำหรับเมทริกซ์ผลลัพธ์)
  2. อธิบายอินพุตและเอาต์พุตสำหรับเชดเดอร์การประมวลผล
  3. คอมไพล์โค้ดตัวปรับแสงประมวลผล
  4. ตั้งค่าไปป์ไลน์การประมวลผล
  5. ส่งคำสั่งที่เข้ารหัสเป็นกลุ่มไปยัง GPU
  6. อ่านบัฟเฟอร์ GPU ของเมทริกซ์ผลลัพธ์

การสร้างบัฟเฟอร์ GPU

เพื่อให้เข้าใจง่าย เมทริกซ์จะแสดงเป็นรายการเลขทศนิยม องค์ประกอบแรกคือจำนวนแถว องค์ประกอบที่ 2 คือจำนวนคอลัมน์ และส่วนที่เหลือคือจำนวนจริงของเมทริกซ์

การนําเสนอเมทริกซ์อย่างง่ายใน JavaScript และเทียบเท่าในเครื่องหมายทางคณิตศาสตร์
การนําเสนอเมทริกซ์อย่างง่ายใน JavaScript และเทียบเท่าในสัญลักษณ์ทางคณิตศาสตร์

บัฟเฟอร์ GPU ทั้ง 3 รายการเป็นบัฟเฟอร์พื้นที่เก็บข้อมูลเนื่องจากเราต้องจัดเก็บและเรียกข้อมูลในตัวปรับเงาการประมวลผล ด้วยเหตุนี้ FLAG การใช้งานบัฟเฟอร์ GPU จึงมี GPUBufferUsage.STORAGE สำหรับรายการทั้งหมด นอกจากนี้ Flag การใช้งานเมทริกซ์ผลลัพธ์ยังมี GPUBufferUsage.COPY_SRC ด้วย เนื่องจากระบบจะคัดลอก Flag นี้ไปยังบัฟเฟอร์อื่นเพื่ออ่านเมื่อระบบเรียกใช้คิว GPU ทั้งหมดแล้ว

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 โดยเฉพาะ เลย์เอาต์กลุ่มการเชื่อมโยงจะกำหนดอินเทอร์เฟซอินพุต/เอาต์พุตที่ตัวสร้างเฉดสีต้องการ ในขณะที่กลุ่มการเชื่อมโยงแสดงถึงข้อมูลอินพุต/เอาต์พุตจริงสำหรับตัวปรับแสงเงา

ในตัวอย่างนี้ เลย์เอาต์กลุ่มการเชื่อมโยงจะคาดหวังว่าจะมีบัฟเฟอร์พื้นที่เก็บข้อมูลแบบอ่านอย่างเดียว 2 รายการที่การเชื่อมโยงรายการที่ 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
      }
    }
  ]
});

โค้ด Shader ของ Compute

โค้ด Shader แบบประมวลผลสำหรับการคูณเมทริกซ์เขียนด้วย WGSL ซึ่งเป็นภาษา Shader ของ WebGPU ที่แปลเป็น SPIR-V ได้โดยง่าย โดยไม่ต้องลงรายละเอียด คุณควรเห็นบัฟเฟอร์พื้นที่เก็บข้อมูล 3 รายการที่ระบุด้วย 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() โดยจะใช้อาร์กิวเมนต์ 2 รายการ ได้แก่ เลย์เอาต์กลุ่มการเชื่อมโยงที่เราสร้างขึ้นก่อนหน้านี้ และระยะการประมวลผลที่กําหนดจุดแรกเข้าของ Shader การประมวลผล (ฟังก์ชัน main WGSL) และโมดูล Shader การประมวลผลจริงที่สร้างด้วย device.createShaderModule()

const computePipeline = device.createComputePipeline({
  layout: device.createPipelineLayout({
    bindGroupLayouts: [bindGroupLayout]
  }),
  compute: {
    module: shaderModule,
    entryPoint: "main"
  }
});

การส่งคําสั่ง

หลังจากสร้างอินสแตนซ์ของกลุ่มการเชื่อมโยงด้วยบัฟเฟอร์ GPU 3 รายการและไปป์ไลน์การประมวลผลที่มีเลย์เอาต์กลุ่มการเชื่อมโยงแล้ว ก็ถึงเวลาใช้งาน

มาเริ่มโปรแกรมเปลี่ยนไฟล์พาสการประมวลผลแบบกำหนดโปรแกรมได้ด้วย commandEncoder.beginComputePass() เราจะใช้สิ่งนี้เพื่อเข้ารหัสคําสั่ง GPU ที่จะทำการดำเนินการคูณเมทริกซ์ ตั้งค่าไปป์ไลน์ด้วย passEncoder.setPipeline(computePipeline) และกลุ่มการเชื่อมโยงที่ดัชนี 0 ด้วย passEncoder.setBindGroup(0, bindGroup) ดัชนี 0 สอดคล้องกับการตกแต่ง group(0) ในรหัส WGSL

ทีนี้มาพูดถึงวิธีที่คอมพิวตเชดเดอร์นี้จะทำงานบน GPU เป้าหมายของเราคือเรียกใช้โปรแกรมนี้แบบขนานกันสำหรับแต่ละเซลล์ของเมทริกซ์ผลลัพธ์ทีละขั้นตอน ตัวอย่างเช่น สำหรับเมทริกซ์ผลลัพธ์ขนาด 16 x 32 หากต้องการเข้ารหัสคำสั่งการดําเนินการใน @workgroup_size(8, 8) เราจะเรียก passEncoder.dispatchWorkgroups(2, 4) หรือ passEncoder.dispatchWorkgroups(16 / 8, 32 / 8) โดยอาร์กิวเมนต์แรก "x" คือมิติข้อมูลแรก อาร์กิวเมนต์ที่ 2 "y" คือมิติข้อมูลที่สอง และอาร์กิวเมนต์สุดท้าย "z" คือมิติข้อมูลที่สามซึ่งมีค่าเริ่มต้นเป็น 1 เนื่องจากเราไม่ได้ใช้มิติข้อมูลนี้ ในการคำนวณด้วย GPU การเข้ารหัสคำสั่งเพื่อเรียกใช้ฟังก์ชันเคอร์เนลในชุดข้อมูลเรียกว่าการจัดเตรียม

การดำเนินการพร้อมกันสำหรับเซลล์เมทริกซ์ผลลัพธ์แต่ละเซลล์
การดำเนินการพร้อมกันสำหรับเซลล์เมทริกซ์ผลลัพธ์แต่ละเซลล์

ขนาดของตารางกริดกลุ่มงานสำหรับตัวปรับแสงเงาการประมวลผลคือ (8, 8) ในโค้ด WGSL ด้วยเหตุนี้ "x" และ "y" ซึ่งเป็นจำนวนแถวของเมทริกซ์แรกและจำนวนคอลัมน์ของเมทริกซ์ที่ 2 ตามลำดับจึงต้องหารด้วย 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() จากนั้นสร้างบัฟเฟอร์ GPU เพื่อใช้เป็นปลายทางในการคัดลอกบัฟเฟอร์เมทริกซ์ผลลัพธ์ด้วย copyBufferToBuffer สุดท้าย ให้เข้ารหัสคําสั่งด้วย copyEncoder.finish() และส่งคําสั่งเหล่านั้นไปยังคิวอุปกรณ์ GPU โดยเรียกใช้ copyEncoder.finish() ด้วยคําสั่ง GPUdevice.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 แล้วรอให้ Promise ที่แสดงผลแสดงผล ซึ่งบ่งบอกว่าตอนนี้มีการแมปบัฟเฟอร์ GPU แล้ว เมื่อถึงจุดนี้ คุณสามารถรับช่วงที่มีการแมปด้วย 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เมธอดที่มีประโยชน์ของไปป์ไลน์การประมวลผลเพื่ออนุมานเลย์เอาต์กลุ่มการเชื่อมโยงจากโมดูล Shader เคล็ดลับนี้ช่วยให้คุณไม่ต้องสร้างเลย์เอาต์กลุ่มการเชื่อมโยงที่กำหนดเองและระบุเลย์เอาต์ไปป์ไลน์ในไปป์ไลน์การประมวลผลตามที่แสดงด้านล่าง

มีภาพ 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 เปรียบเทียบกับการทำงานใน CPU อย่างไร เราจึงเขียนโปรแกรมที่อธิบายไปสำหรับ CPU เพื่อหาคำตอบ และดังที่คุณเห็นในกราฟด้านล่าง การใช้พลังของ GPU อย่างเต็มรูปแบบดูเหมือนจะเป็นตัวเลือกที่ชัดเจนเมื่อขนาดของเมทริกซ์มากกว่า 256 x 256

การเปรียบเทียบ GPU กับ CPU
การเปรียบเทียบประสิทธิภาพ GPU กับ CPU

บทความนี้เป็นเพียงจุดเริ่มต้นของเส้นทางการสำรวจ WebGPU โปรดรอบทความเพิ่มเติมในเร็วๆ นี้ที่เจาะลึกเรื่อง GPU Compute และวิธีการทำงานของการแสดงผล (Canvas, Texture, Sampler) ใน WebGPU