synor/sdk/js/src/tensor.ts
Gulshan Yadav a808bb37a6 feat(sdk): add consumer SDKs for JavaScript, Python, and Go
Add complete SDK implementations for accessing Synor Compute:

JavaScript/TypeScript SDK (sdk/js/):
- Full async/await API with TypeScript types
- Tensor operations: matmul, conv2d, attention
- Model inference with streaming support
- WebSocket-based job monitoring
- Browser and Node.js compatible

Python SDK (sdk/python/):
- Async/await with aiohttp
- NumPy integration for tensors
- Context managers for cleanup
- Type hints throughout
- PyPI-ready package structure

Go SDK (sdk/go/):
- Idiomatic Go with context support
- Efficient binary tensor serialization
- HTTP client with configurable timeouts
- Zero external dependencies (stdlib only)

All SDKs support:
- Matrix multiplication (FP64 to INT4 precision)
- Convolution operations (2D, 3D)
- Flash attention
- LLM inference
- Spot pricing queries
- Job polling and cancellation
- Heterogeneous compute targeting (CPU/GPU/TPU/NPU/LPU)
2026-01-11 14:11:58 +05:30

284 lines
7.6 KiB
TypeScript

/**
* Tensor utilities for Synor Compute SDK
*/
import type { TensorData, TensorShape, Precision } from './types';
/**
* Tensor wrapper for compute operations.
*/
export class Tensor {
readonly data: Float32Array | Float64Array | Int32Array | Int8Array | Uint8Array;
readonly shape: number[];
readonly dtype: Precision;
private constructor(
data: Float32Array | Float64Array | Int32Array | Int8Array | Uint8Array,
shape: number[],
dtype: Precision
) {
this.data = data;
this.shape = shape;
this.dtype = dtype;
}
/**
* Create a tensor from various input formats.
*/
static from(data: TensorData, dtype: Precision = 'fp32'): Tensor {
if (data instanceof Float32Array) {
return new Tensor(data, [data.length], 'fp32');
}
if (data instanceof Float64Array) {
return new Tensor(data, [data.length], 'fp64');
}
if (data instanceof Int32Array) {
return new Tensor(data, [data.length], 'int8');
}
if (data instanceof Int8Array) {
return new Tensor(data, [data.length], 'int8');
}
if (data instanceof Uint8Array) {
return new Tensor(data, [data.length], 'int8');
}
// Handle nested arrays
const shape = Tensor.inferShape(data);
const flat = Tensor.flatten(data);
const typedArray = dtype === 'fp64'
? new Float64Array(flat)
: dtype === 'fp32'
? new Float32Array(flat)
: dtype === 'int8' || dtype === 'int4'
? new Int8Array(flat)
: new Float32Array(flat);
return new Tensor(typedArray, shape, dtype);
}
/**
* Create a tensor of zeros.
*/
static zeros(shape: number[], dtype: Precision = 'fp32'): Tensor {
const size = shape.reduce((a, b) => a * b, 1);
const data = dtype === 'fp64'
? new Float64Array(size)
: dtype === 'fp32'
? new Float32Array(size)
: new Int8Array(size);
return new Tensor(data, shape, dtype);
}
/**
* Create a tensor of ones.
*/
static ones(shape: number[], dtype: Precision = 'fp32'): Tensor {
const size = shape.reduce((a, b) => a * b, 1);
const data = dtype === 'fp64'
? new Float64Array(size).fill(1)
: dtype === 'fp32'
? new Float32Array(size).fill(1)
: new Int8Array(size).fill(1);
return new Tensor(data, shape, dtype);
}
/**
* Create a tensor with random values.
*/
static random(shape: number[], dtype: Precision = 'fp32'): Tensor {
const size = shape.reduce((a, b) => a * b, 1);
const data = dtype === 'fp64'
? Float64Array.from({ length: size }, () => Math.random())
: dtype === 'fp32'
? Float32Array.from({ length: size }, () => Math.random())
: Int8Array.from({ length: size }, () => Math.floor(Math.random() * 256) - 128);
return new Tensor(data, shape, dtype);
}
/**
* Create a tensor with normal distribution.
*/
static randn(shape: number[], dtype: Precision = 'fp32'): Tensor {
const size = shape.reduce((a, b) => a * b, 1);
const values: number[] = [];
for (let i = 0; i < size; i++) {
// Box-Muller transform
const u1 = Math.random();
const u2 = Math.random();
values.push(Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2));
}
const data = dtype === 'fp64'
? new Float64Array(values)
: dtype === 'fp32'
? new Float32Array(values)
: new Int8Array(values.map(v => Math.max(-128, Math.min(127, Math.round(v * 50)))));
return new Tensor(data, shape, dtype);
}
/**
* Get tensor size (total elements).
*/
get size(): number {
return this.shape.reduce((a, b) => a * b, 1);
}
/**
* Get number of dimensions.
*/
get ndim(): number {
return this.shape.length;
}
/**
* Get byte size.
*/
get byteSize(): number {
const bytesPerElement = this.dtype === 'fp64' ? 8
: this.dtype === 'fp32' ? 4
: this.dtype === 'fp16' || this.dtype === 'bf16' ? 2
: 1;
return this.size * bytesPerElement;
}
/**
* Reshape tensor.
*/
reshape(newShape: number[]): Tensor {
const newSize = newShape.reduce((a, b) => a * b, 1);
if (newSize !== this.size) {
throw new Error(`Cannot reshape tensor of size ${this.size} to shape [${newShape}]`);
}
return new Tensor(this.data, newShape, this.dtype);
}
/**
* Convert to different precision.
*/
to(dtype: Precision): Tensor {
if (dtype === this.dtype) return this;
const newData = dtype === 'fp64'
? Float64Array.from(this.data)
: dtype === 'fp32'
? Float32Array.from(this.data)
: Int8Array.from(this.data);
return new Tensor(newData, this.shape, dtype);
}
/**
* Get value at index.
*/
get(...indices: number[]): number {
const idx = this.flatIndex(indices);
return this.data[idx];
}
/**
* Set value at index.
*/
set(value: number, ...indices: number[]): void {
const idx = this.flatIndex(indices);
this.data[idx] = value;
}
/**
* Convert to nested array.
*/
toArray(): number[] | number[][] | number[][][] {
if (this.ndim === 1) {
return Array.from(this.data);
}
const result: any[] = [];
const innerSize = this.shape.slice(1).reduce((a, b) => a * b, 1);
for (let i = 0; i < this.shape[0]; i++) {
const slice = new Tensor(
this.data.slice(i * innerSize, (i + 1) * innerSize) as Float32Array,
this.shape.slice(1),
this.dtype
);
result.push(slice.toArray());
}
return result;
}
/**
* Serialize tensor for transmission.
*/
serialize(): { data: string; shape: number[]; dtype: Precision } {
// Convert to base64 for efficient transmission
const bytes = new Uint8Array(this.data.buffer);
const base64 = btoa(String.fromCharCode(...bytes));
return {
data: base64,
shape: this.shape,
dtype: this.dtype,
};
}
/**
* Deserialize tensor from transmission format.
*/
static deserialize(serialized: { data: string; shape: number[]; dtype: Precision }): Tensor {
const binary = atob(serialized.data);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
const data = serialized.dtype === 'fp64'
? new Float64Array(bytes.buffer)
: serialized.dtype === 'fp32'
? new Float32Array(bytes.buffer)
: new Int8Array(bytes.buffer);
return new Tensor(data, serialized.shape, serialized.dtype);
}
/**
* Calculate flat index from multi-dimensional indices.
*/
private flatIndex(indices: number[]): number {
if (indices.length !== this.ndim) {
throw new Error(`Expected ${this.ndim} indices, got ${indices.length}`);
}
let idx = 0;
let stride = 1;
for (let i = this.ndim - 1; i >= 0; i--) {
if (indices[i] < 0 || indices[i] >= this.shape[i]) {
throw new Error(`Index ${indices[i]} out of bounds for axis ${i} with size ${this.shape[i]}`);
}
idx += indices[i] * stride;
stride *= this.shape[i];
}
return idx;
}
/**
* Infer shape from nested array.
*/
private static inferShape(data: number[] | number[][]): number[] {
if (!Array.isArray(data)) return [];
if (!Array.isArray(data[0])) return [data.length];
return [data.length, ...Tensor.inferShape(data[0] as number[] | number[][])];
}
/**
* Flatten nested array.
*/
private static flatten(data: number[] | number[][]): number[] {
if (!Array.isArray(data)) return [data as unknown as number];
return (data as any[]).flatMap(item =>
Array.isArray(item) ? Tensor.flatten(item) : item
);
}
}