UNPKG

greed.js

Version:

Lightweight, private alternative to Colab. Run PyTorch & NumPy in browser with GPU acceleration (8.8x speedup). Fast, secure, runs locally.

786 lines (662 loc) 22.1 kB
/** * WebGPUTensor - GPU-accelerated tensor implementation * Replaces the numpy-based WebGPUTensor with actual WebGPU operations */ import logger from '../../utils/logger.js'; export class WebGPUTensor { constructor(data, options = {}) { // Tensor properties this.device = options.device || 'webgpu'; this.dtype = options.dtype || 'float32'; this.requires_grad = options.requires_grad || false; this.grad = null; this.grad_fn = null; if (Array.isArray(data) || ArrayBuffer.isView(data)) { this.data = this._processInputData(data); this.shape = options.shape || this._inferShape(data); } else if (data instanceof ArrayBuffer) { this.data = new Float32Array(data); this.shape = options.shape || [this.data.length]; } else if (data && typeof data === 'object' && data.constructor?.name === 'PyProxy' && typeof data.length !== 'undefined') { // Handle Pyodide PyProxy objects (Python lists converted to JavaScript) const jsArray = Array.from(data); // Convert PyProxy to native JS array this.data = this._processInputData(jsArray); this.shape = options.shape || this._inferShape(jsArray); } else { const debugInfo = { type: typeof data, constructor: data?.constructor?.name, isArray: Array.isArray(data), isArrayBufferView: ArrayBuffer.isView(data), isArrayBuffer: data instanceof ArrayBuffer, hasLength: typeof data?.length !== 'undefined', stringValue: String(data).substring(0, 100) }; throw new Error(`Invalid tensor data type: ${JSON.stringify(debugInfo)}`); } // Derived properties this.ndim = this.shape.length; this.size = this.shape.reduce((a, b) => a * b, 1); // WebGPU compute engine reference this.computeEngine = options.computeEngine || null; this._buffer = null; // GPU buffer cache this._isOnGPU = false; } /** * Get reference to WebGPU compute engine */ static setComputeEngine(engine) { WebGPUTensor.globalComputeEngine = engine; } get _engine() { return this.computeEngine || WebGPUTensor.globalComputeEngine; } /** * Execute WebGPU operation */ async _executeGPUOperation(operation, other = null, options = {}) { if (!this._engine) { throw new Error('WebGPU compute engine not available'); } // Safely extract tensor data, handling potential proxy destruction let otherData = null; if (other) { try { otherData = other.data || other; // If it's a proxy that might be destroyed, copy it immediately if (otherData && typeof otherData === 'object' && otherData.constructor?.name === 'PyProxy') { otherData = Array.from(otherData); otherData = new Float32Array(otherData); } } catch (error) { throw new Error(`Failed to access tensor data: ${error.message}. This may be due to Pyodide proxy destruction.`); } } const tensors = other ? [this.data, otherData] : [this.data]; try { const result = await this._engine.execute(operation, tensors, { shape: this.shape, otherShape: other?.shape, dtype: this.dtype, ...options }); const resultShape = this._calculateResultShape(operation, other, options); const resultTensor = new WebGPUTensor(result, { shape: resultShape, device: this.device, dtype: this.dtype, computeEngine: this._engine, requires_grad: this.requires_grad || (other?.requires_grad) }); return resultTensor; } catch (error) { const fallbackInfo = { operation, error: error.message, tensorShape: this.shape, otherShape: other?.shape, fallbackReason: 'gpu-operation-failed', engineAvailable: !!this._engine, engineInitialized: this._engine?.isInitialized }; logger.warn(`WebGPU operation ${operation} failed, falling back to CPU:`, fallbackInfo); return this._executeCPUFallback(operation, other, options); } } /** * CPU fallback for when WebGPU operations fail */ _executeCPUFallback(operation, other, options) { // Implement basic CPU operations as fallback const result = this._cpuOperations[operation]?.(this.data, other?.data || other, options); if (!result) { throw new Error(`Operation ${operation} not supported`); } const resultShape = this._calculateResultShape(operation, other, options); return new WebGPUTensor(result, { shape: resultShape, device: 'cpu', dtype: this.dtype, requires_grad: this.requires_grad || (other?.requires_grad) }); } // ===== ARITHMETIC OPERATIONS ===== async add(other) { return this._executeGPUOperation('add', other); } async sub(other) { return this._executeGPUOperation('sub', other); } async mul(other) { return this._executeGPUOperation('mul', other); } async div(other) { return this._executeGPUOperation('div', other); } async pow(exponent) { return this._executeGPUOperation('pow', exponent); } // ===== MATRIX OPERATIONS ===== async matmul(other) { if (this.ndim !== 2 || other.ndim !== 2) { throw new Error('matmul requires 2D tensors'); } if (this.shape[1] !== other.shape[0]) { throw new Error(`Cannot multiply matrices of shapes ${this.shape} and ${other.shape}`); } return this._executeGPUOperation('matmul', other); } async bmm(other) { if (this.ndim !== 3 || other.ndim !== 3) { throw new Error('bmm requires 3D tensors'); } return this._executeGPUOperation('bmm', other); } async transpose(dim0 = 0, dim1 = 1) { return this._executeGPUOperation('transpose', null, { dim0, dim1 }); } // ===== ACTIVATION FUNCTIONS ===== async relu() { return this._executeGPUOperation('relu'); } async leaky_relu(negative_slope = 0.01) { return this._executeGPUOperation('leaky_relu', null, { negativeSlope: negative_slope }); } async sigmoid() { return this._executeGPUOperation('sigmoid'); } async tanh() { return this._executeGPUOperation('tanh'); } async gelu() { return this._executeGPUOperation('gelu'); } async softmax(dim = -1) { return this._executeGPUOperation('softmax', null, { dim }); } async log_softmax(dim = -1) { return this._executeGPUOperation('log_softmax', null, { dim }); } // ===== REDUCTION OPERATIONS ===== async sum(dim = null, keepdim = false) { return this._executeGPUOperation('sum', null, { dim, keepDim: keepdim }); } async mean(dim = null, keepdim = false) { return this._executeGPUOperation('mean', null, { dim, keepDim: keepdim }); } async max(dim = null, keepdim = false) { if (dim === null) { // Global max const reduced = await this._executeGPUOperation('max_reduce'); return reduced; } else { // Max along dimension - returns values and indices return this._executeGPUOperation('max', null, { dim, keepDim: keepdim }); } } async min(dim = null, keepdim = false) { if (dim === null) { const reduced = await this._executeGPUOperation('min_reduce'); return reduced; } else { return this._executeGPUOperation('min', null, { dim, keepDim: keepdim }); } } // ===== STATISTICAL OPERATIONS ===== async std(dim = null, keepdim = false, unbiased = true) { return this._executeGPUOperation('std', null, { dim, keepDim: keepdim, unbiased }); } async var(dim = null, keepdim = false, unbiased = true) { return this._executeGPUOperation('var', null, { dim, keepDim: keepdim, unbiased }); } // ===== MATHEMATICAL FUNCTIONS ===== async exp() { return this._executeGPUOperation('exp'); } async log() { return this._executeGPUOperation('log'); } async sqrt() { return this._executeGPUOperation('sqrt'); } async abs() { return this._executeGPUOperation('abs'); } async clamp(min = null, max = null) { return this._executeGPUOperation('clamp', null, { min, max }); } // ===== TENSOR MANIPULATION ===== view(...shape) { const newSize = shape.reduce((a, b) => a * b, 1); if (newSize !== this.size) { throw new Error(`Cannot reshape tensor of size ${this.size} to shape ${shape}`); } return new WebGPUTensor(this.data, { shape: shape, device: this.device, dtype: this.dtype, computeEngine: this._engine, requires_grad: this.requires_grad }); } reshape(...shape) { return this.view(...shape); } unsqueeze(dim) { const newShape = [...this.shape]; if (dim < 0) dim = newShape.length + dim + 1; newShape.splice(dim, 0, 1); return this.view(...newShape); } squeeze(dim = null) { let newShape; if (dim === null) { newShape = this.shape.filter(s => s !== 1); } else { if (this.shape[dim] !== 1) { throw new Error(`Cannot squeeze dimension ${dim} of size ${this.shape[dim]}`); } newShape = [...this.shape]; newShape.splice(dim, 1); } return this.view(...newShape); } flatten(start_dim = 0, end_dim = -1) { if (end_dim === -1) end_dim = this.ndim - 1; const beforeDims = this.shape.slice(0, start_dim); const flattenDims = this.shape.slice(start_dim, end_dim + 1); const afterDims = this.shape.slice(end_dim + 1); const flattenedSize = flattenDims.reduce((a, b) => a * b, 1); const newShape = [...beforeDims, flattenedSize, ...afterDims]; return this.view(...newShape); } // ===== DEVICE OPERATIONS ===== to(device) { if (device === this.device) return this; return new WebGPUTensor(this.data.slice(), { shape: this.shape, device: device, dtype: this.dtype, computeEngine: this._engine, requires_grad: this.requires_grad }); } cpu() { return this.to('cpu'); } cuda() { return this.to('webgpu'); // Map CUDA to WebGPU } // ===== AUTOGRAD SUPPORT ===== retain_grad() { if (!this.requires_grad) { throw new Error('can\'t retain_grad on Tensor that has requires_grad=False'); } this._retain_grad = true; return this; } backward(gradient = null, retain_graph = false, create_graph = false) { if (!this.requires_grad) { return; } if (gradient === null) { if (this.size === 1) { gradient = new WebGPUTensor([1.0], { shape: this.shape }); } else { throw new Error('grad can be implicitly created only for scalar outputs'); } } // Initialize gradient if not present if (this.grad === null) { this.grad = new WebGPUTensor(new Float32Array(this.size).fill(0), { shape: this.shape, device: this.device, dtype: this.dtype }); } // Accumulate gradient const gradData = gradient.data || gradient; for (let i = 0; i < this.grad.data.length; i++) { this.grad.data[i] += Array.isArray(gradData) ? gradData[i] : gradData; } if (this.grad_fn) { this.grad_fn(gradient); } } // ===== UTILITY METHODS ===== numpy() { return this.data; } tolist() { if (this.ndim === 1) { return Array.from(this.data); } // For multi-dimensional arrays, recursively convert return this._arrayToNestedList(this.data, this.shape); } item() { if (this.size !== 1) { throw new Error('item() can only be called on tensors with one element'); } return this.data[0]; } clone() { return new WebGPUTensor(this.data.slice(), { shape: this.shape, device: this.device, dtype: this.dtype, computeEngine: this._engine, requires_grad: this.requires_grad }); } detach() { const detached = this.clone(); detached.requires_grad = false; detached.grad_fn = null; return detached; } // ===== PRIVATE METHODS ===== _isDebugEnabled() { // Check for WebGPU debug flag in global scope try { return (typeof window !== 'undefined' && window.greedDebugWebGPU) || (typeof global !== 'undefined' && global.greedDebugWebGPU); } catch { return false; } } _processInputData(data) { if (Array.isArray(data)) { return new Float32Array(this._flattenArray(data)); } else if (ArrayBuffer.isView(data)) { return new Float32Array(data); } else { throw new Error('Unsupported data type'); } } _flattenArray(arr) { const result = []; const flatten = (item) => { if (Array.isArray(item)) { item.forEach(flatten); } else { result.push(Number(item)); } }; flatten(arr); return result; } _inferShape(data) { if (!Array.isArray(data)) { return [data.length || 1]; } const getShape = (arr) => { if (!Array.isArray(arr)) return []; const shape = [arr.length]; if (arr.length > 0 && Array.isArray(arr[0])) { shape.push(...getShape(arr[0])); } return shape; }; return getShape(data); } _calculateResultShape(operation, other, options) { switch (operation) { case 'matmul': return [this.shape[0], other.shape[1]]; case 'bmm': return [this.shape[0], this.shape[1], other.shape[2]]; case 'transpose': const newShape = [...this.shape]; const { dim0 = 0, dim1 = 1 } = options; [newShape[dim0], newShape[dim1]] = [newShape[dim1], newShape[dim0]]; return newShape; case 'sum': case 'mean': if (options.dim === null) { return options.keepDim ? this.shape : [1]; } else { const newShape = [...this.shape]; if (options.keepDim) { newShape[options.dim] = 1; } else { newShape.splice(options.dim, 1); } return newShape.length === 0 ? [1] : newShape; } case 'softmax': case 'log_softmax': return this.shape; default: // Element-wise operations preserve shape return this.shape; } } _arrayToNestedList(data, shape) { if (shape.length === 1) { return Array.from(data); } const result = []; const stride = shape.slice(1).reduce((a, b) => a * b, 1); for (let i = 0; i < shape[0]; i++) { const start = i * stride; const end = start + stride; const subData = data.slice(start, end); result.push(this._arrayToNestedList(subData, shape.slice(1))); } return result; } // CPU fallback operations get _cpuOperations() { return { add: (a, b) => a.map((val, i) => val + (Array.isArray(b) ? b[i] : b)), sub: (a, b) => a.map((val, i) => val - (Array.isArray(b) ? b[i] : b)), mul: (a, b) => a.map((val, i) => val * (Array.isArray(b) ? b[i] : b)), div: (a, b) => a.map((val, i) => val / (Array.isArray(b) ? b[i] : b)), pow: (a, b) => a.map((val, i) => Math.pow(val, Array.isArray(b) ? b[i] : b)), exp: (a) => a.map(val => Math.exp(val)), log: (a) => a.map(val => Math.log(val)), sqrt: (a) => a.map(val => Math.sqrt(val)), abs: (a) => a.map(val => Math.abs(val)), clamp: (a, b, options = {}) => { const { min = null, max = null } = options; return a.map(val => { if (min !== null && val < min) return min; if (max !== null && val > max) return max; return val; }); }, relu: (a) => a.map(val => Math.max(0, val)), sigmoid: (a) => a.map(val => 1 / (1 + Math.exp(-val))), tanh: (a) => a.map(val => Math.tanh(val)), softmax: (a, options = {}) => { const max = Math.max(...a); const exps = a.map(val => Math.exp(val - max)); const sum = exps.reduce((acc, val) => acc + val, 0); return exps.map(val => val / sum); }, log_softmax: (a, options = {}) => { const max = Math.max(...a); const exps = a.map(val => Math.exp(val - max)); const sum = exps.reduce((acc, val) => acc + val, 0); const logSumExp = Math.log(sum); return a.map(val => (val - max) - logSumExp); }, matmul: (a, b, options) => { // CPU matrix multiplication fallback const aShape = options.shape || [Math.sqrt(a.length), Math.sqrt(a.length)]; const bShape = options.otherShape || [Math.sqrt(b.length), Math.sqrt(b.length)]; if (aShape.length !== 2 || bShape.length !== 2) { throw new Error('CPU matmul fallback requires 2D matrices'); } const [M, K] = aShape; const [K2, N] = bShape; if (K !== K2) { throw new Error(`Cannot multiply matrices of shapes [${M},${K}] and [${K2},${N}]`); } const result = new Array(M * N); for (let i = 0; i < M; i++) { for (let j = 0; j < N; j++) { let sum = 0; for (let k = 0; k < K; k++) { sum += a[i * K + k] * b[k * N + j]; } result[i * N + j] = sum; } } return result; }, std: (a, options = {}) => { const mean = a.reduce((sum, val) => sum + val, 0) / a.length; const variance = a.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / (options.unbiased ? a.length - 1 : a.length); return [Math.sqrt(variance)]; }, var: (a, options = {}) => { const mean = a.reduce((sum, val) => sum + val, 0) / a.length; const variance = a.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / (options.unbiased ? a.length - 1 : a.length); return [variance]; } }; } toString() { return `WebGPUTensor(${this.shape.join('x')}, device=${this.device}, dtype=${this.dtype})`; } [Symbol.toPrimitive](hint) { if (hint === 'number' && this.size === 1) { return this.data[0]; } return this.toString(); } // ===== OPERATOR OVERLOADING ===== // Matrix multiplication operator (@) - requires custom implementation ['@'](other) { return this.matmul(other); } // Add operator overloading for Python-style operations __add__(other) { return this.add(other); } __sub__(other) { return this.sub(other); } __mul__(other) { return this.mul(other); } __truediv__(other) { return this.div(other); } __matmul__(other) { return this.matmul(other); } // In-place operations (modify tensor in place and return self) // These are required by PyTorch optimizers mul_(other) { const result = this.mul(other); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; return this; } add_(other, alpha = 1) { // PyTorch's add_ supports: tensor.add_(other, alpha=1) which does: tensor += alpha * other if (alpha !== 1) { const scaled = typeof other === 'number' ? other * alpha : this.mul(other, alpha); const result = this.add(scaled); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; } else { const result = this.add(other); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; } return this; } sub_(other, alpha = 1) { // PyTorch's sub_ supports: tensor.sub_(other, alpha=1) which does: tensor -= alpha * other if (alpha !== 1) { const scaled = typeof other === 'number' ? other * alpha : this.mul(other, alpha); const result = this.sub(scaled); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; } else { const result = this.sub(other); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; } return this; } div_(other) { const result = this.div(other); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; return this; } pow_(exponent) { const result = this.pow(exponent); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; return this; } sqrt_() { const result = this.sqrt(); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; return this; } addcmul_(tensor1, tensor2, value = 1) { // PyTorch's addcmul_: tensor += value * tensor1 * tensor2 const mul_result = tensor1.mul(tensor2); const scaled = value !== 1 ? mul_result.mul(value) : mul_result; const result = this.add(scaled); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; return this; } addcdiv_(tensor1, tensor2, value = 1) { // PyTorch's addcdiv_: tensor += value * tensor1 / tensor2 const div_result = tensor1.div(tensor2); const scaled = value !== 1 ? div_result.mul(value) : div_result; const result = this.add(scaled); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; return this; } clamp_(min, max) { const result = this.clamp(min, max); this._data = result._data; this.shape = result.shape; this.dtype = result.dtype; return this; } zero_() { // Fill tensor with zeros in-place this._data.fill(0); return this; } fill_(value) { // Fill tensor with a specific value in-place this._data.fill(value); return this; } copy_(other) { // Copy data from another tensor in-place if (other instanceof WebGPUTensor) { this._data = new Float32Array(other._data); this.shape = [...other.shape]; this.dtype = other.dtype; } else { throw new Error('copy_ requires a WebGPUTensor'); } return this; } } export default WebGPUTensor;