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

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

François Beaufort
François Beaufort

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

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

GPU Compute มีส่วนอย่างมากต่อการเติบโตของแมชชีนเลิร์นนิงเมื่อเร็วๆ นี้ เนื่องจากโครงข่ายประสาทแบบคอนโวลูชัน (Convolution) และโมเดลอื่นๆ สามารถใช้ประโยชน์จาก เพื่อให้ทำงานได้อย่างมีประสิทธิภาพบน GPU ด้วยแพลตฟอร์มเว็บปัจจุบัน ขาดความสามารถของ GPU Compute, "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();

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

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

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

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

การเขียนโปรแกรม Shader

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

เพื่อแสดงให้เห็นถึงการใช้ตัวปรับแสงเงาการประมวลผลใน WebGPU เราจะลองใช้เมทริกซ์ ซึ่งเป็นอัลกอริทึมที่พบบ่อยในแมชชีนเลิร์นนิงตามที่แสดงด้านล่าง

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

กล่าวโดยสรุปคือ สิ่งที่เราจะทำมีดังต่อไปนี้

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

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

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

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

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

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

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

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

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

มาเริ่มโปรแกรมเปลี่ยนไฟล์สำหรับ Compute Pass ที่สามารถตั้งโปรแกรมได้ด้วย 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" มิติข้อมูลแรกคือมิติข้อมูลแรก มิติข้อมูลที่สองเป็น "y" คือมิติข้อมูลที่ 2 และอันล่าสุดคือ "z" คือมิติข้อมูลที่สามที่มีค่าเริ่มต้นเป็น 1 เนื่องจากเราไม่ต้องการข้อมูลนี้ ในโลกการประมวลผล GPU การเข้ารหัสคำสั่งเพื่อเรียกใช้ฟังก์ชันเคอร์เนลในชุดข้อมูลเรียกว่าการจ่ายงาน

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

ขนาดของตารางกริดกลุ่มงานสำหรับตัวปรับแสงเงาการประมวลผลคือ (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() จากนั้นสร้าง บัฟเฟอร์ GPU ที่จะใช้เป็นปลายทางในการคัดลอกบัฟเฟอร์เมทริกซ์ผลลัพธ์ copyBufferToBuffer สุดท้าย จบคำสั่งการเข้ารหัสด้วย copyEncoder.finish() และส่งไปยังคิวอุปกรณ์ GPU โดยการเรียกใช้ device.queue.submit() ด้วยคำสั่ง GPU

// 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: [

ผลการสืบค้นประสิทธิภาพ

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

วันที่ การเปรียบเทียบ GPU เทียบกับ CPU
การเปรียบเทียบระหว่าง GPU กับ CPU

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