Menjalankan konversi ruang warna masif menggunakan WebGPU compute shader sering kali menghasilkan deviasi nilai piksel jika dibandingkan dengan implementasi referensi berbasis CPU. Perbedaan arsitektur perangkat keras, optimasi driver, dan representasi floating-point memicu variasi hasil render. Artikel ini membahas penyebab teknis deviasi tersebut serta implementasi pengujian regresi otomatis berbasis ambang batas numerik (epsilon) di pipeline CI.
Penyebab Floating Point Drift: CPU vs. GPU
Penyimpangan numerik pada pemrosesan warna umumnya berakar dari tiga faktor teknis utama pada tingkat arsitektur:
- Standar IEEE 754 vs Fast-Math GPU: Sebagian besar CPU modern menjalankan operasi matematika berbasis
f64atauf32yang ketat terhadap standar IEEE 754. Sebaliknya, driver GPU sering kali mengaktifkan optimasi aproksimasi cepat (seperti implementasi non-standar untuk fungsipow(),exp(), atau inversi matriks) untuk mengejar throughput pemrosesan. - Fused Multiply-Add (FMA): Arsitektur GPU modern menggabungkan operasi perkalian dan penambahan menjadi satu siklus (
a * b + c) tanpa fase pembulatan perantara. Ini meningkatkan akurasi internal GPU namun menghasilkan delta numerik jika dibandingkan dengan CPU yang mengeksekusi instruksi perkalian dan penjumlahan secara terpisah. - Akumulasi Rounding Bias pada Transfer Ruang Warna: Transformasi ruang warna non-linear (misalnya sRGB ke Oklab atau Display P3 ke ACEScg) membutuhkan operasi transendental berulang. Kesalahan presisi terkecil pada satu komponen matriks akan terakumulasi secara eksponensial di ruang persepsi.
Strategi Pengujian Numerik: Golden Master dan Epsilon Thresholding
Memverifikasi akurasi konversi warna GPU tidak dapat mengandalkan perbandingan nilai eksak (strict equality). Nilai assert(gpuColor === cpuColor) dipastikan gagal.
Metode yang tepat mencakup:
- Golden Master Testing: Menghasilkan dataset input biner standar yang mencakup seluruh rentang dinamis warna (0.0 hingga 1.0) beserta edge cases (nilai di luar rentang, nilai mendekati nol, dan saturasi maksimal).
- Delta Epsilon Absolute & Relative: Evaluasi perbedaan komponen menggunakan kombinasi error absolut dan toleransi error relatif:
diff = abs(val_gpu - val_ref) <= max(abs_tol, rel_tol * abs(val_ref)) - Delta E Metric (CIE ΔE 2000 atau ΔE Oklab): Alih-alih hanya menguji nilai RGB mentah, deviasi dikonversi ke satuan persepsi visual. Jika ΔE < 0.001 (di bawah ambang batas persepsi manusia), konversi dinyatakan lulus uji regresi.
Test Harness Minimal: Node.js, WebGPU, dan Strict Assert
Berikut implementasi harness pengujian regresi numerik konversi RGB ke Linear RGB menggunakan WebGPU compute pass di Node.js (memerlukan runtime yang mendukung WebGPU headless seperti node-webgpu atau engine berbasis Chromium headless).
import assert from 'node:assert/strict';
// 1. Formula referensi CPU (IEEE 754 standar f64 / f32)
function srgbToLinearCPU(val) {
return val <= 0.04045
? val / 12.92
: Math.pow((val + 0.055) / 1.055, 2.4);
}
// 2. Compute Shader WGSL
const shaderWGSL = /* wgsl */`
@group(0) @binding(0) var<storage, read> inputBuf: array<f32>;
@group(0) @binding(1) var<storage, read_write> outputBuf: array<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let idx = id.x;
let val = inputBuf[idx];
// Implementasi sRGB transfer function pada GPU
if (val <= 0.04045) {
outputBuf[idx] = val / 12.92;
} else {
outputBuf[idx] = pow((val + 0.055) / 1.055, 2.4);
}
}
`;
async function runRegressionSuite() {
const adapter = await navigator.gpu.requestAdapter();
assert.ok(adapter, 'Adapter GPU tidak ditemukan');
const device = await adapter.requestDevice();
// Dataset golden master: 256 nilai sRGB diskrit
const testSize = 256;
const inputData = new Float32Array(testSize);
const expectedCPU = new Float32Array(testSize);
for (let i = 0; i < testSize; i++) {
const srgb = i / (testSize - 1);
inputData[i] = srgb;
expectedCPU[i] = srgbToLinearCPU(srgb);
}
// Alokasi buffer WebGPU
const byteSize = inputData.byteLength;
const gpuInBuf = device.createBuffer({
size: byteSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
});
const gpuOutBuf = device.createBuffer({
size: byteSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});
const readBuf = device.createBuffer({
size: byteSize,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
});
device.queue.writeBuffer(gpuInBuf, 0, inputData);
// Setup pipeline compute
const module = device.createShaderModule({ code: shaderWGSL });
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: { module, entryPoint: 'main' }
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: gpuInBuf } },
{ binding: 1, resource: { buffer: gpuOutBuf } }
]
});
// Eksekusi compute pass
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(Math.ceil(testSize / 64));
pass.end();
encoder.copyBufferToBuffer(gpuOutBuf, 0, readBuf, 0, byteSize);
device.queue.submit([encoder.finish()]);
// Baca hasil kembali ke CPU
await readBuf.mapAsync(GPUMapMode.READ);
const gpuResults = new Float32Array(readBuf.getMappedRange().slice(0));
readBuf.unmap();
// Validasi toleransi epsilon f32
const EPSILON = 1e-5;
let maxDelta = 0;
for (let i = 0; i < testSize; i++) {
const diff = Math.abs(gpuResults[i] - expectedCPU[i]);
if (diff > maxDelta) maxDelta = diff;
assert.ok(
diff <= EPSILON,
`Regresi numerik terdeteksi pada indeks ${i} (input: ${inputData[i]}): GPU=${gpuResults[i]}, CPU=${expectedCPU[i]}, Delta=${diff}`
);
}
console.log(`Uji selesai. Max Delta GPU-CPU: ${maxDelta.toExponential(4)} (Epsilon: ${EPSILON})`);
}
runRegressionSuite().catch(err => {
console.error(err);
process.exit(1);
});
Integrasi pada Pipeline CI Headless
Menjalankan shader WebGPU atau WebGL di environment Continuous Integration (seperti GitHub Actions atau GitLab CI) sering terkendala ketiadaan GPU fisik. Solusinya adalah memanfaatkan software rasterizer.
1. Konfigurasi Software Rasterizer (Vulkan / Mesa SwiftShader)
Gunakan runtime Chromium headless atau Node binding yang diarahkan ke driver software Mesa seperti llvmpipe atau SwiftShader:
# Contoh eksekusi test pada Ubuntu runner
sudo apt-get update && sudo apt-get install -y mesa-vulkan-drivers libgl1-mesa-dri
export VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/swiftshader_icd.json
node run-gpu-tests.mjs
2. Snapshot Testing Snapshot Biner
Simpan dataset referensi CPU dalam format file biner terkompresi (misalnya .bin atau .raw) di repository. Selama eksekusi CI, muat file referensi ini dan bandingkan dengan buffer output GPU menggunakan hash buffer atau delta sweep. Pendekatan ini mencegah fluktuasi akurasi akibat perubahan implementasi compiler CPU antar-versi runtime.
Praktik Terbaik Mencegah Regresi
- Standarisasi Type Casting WGSL: Hindari konversi implisit. Selalu deklarasikan literal float secara eksplisit sebagai
f32(misal:1.0f) untuk mencegah compiler WebGPU salah menentukan precision class. - Tangani Kondisi Khusus (Edge Cases): Periksa perilaku fungsi
pow()terhadap nilai dasar negatif atau 0.0 di hardware yang berbeda. Berikan guard clause pada shader jika hardware target berpotensi menghasilkanNaN. - Catat Fluktuasi Antar-Vendor: Jika pengujian dilakukan di GPU fisik lokal (Nvidia, AMD, Apple Silicon), simpan metadata vendor GPU. Driver proprietary Nvidia sering menggunakan profil fast-math yang berbeda dari implementasi driver Metal di macOS.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!