7254f7b4d1
Build / Build (macos-latest) (push) Has been cancelled
Build / Build (ubuntu-latest) (push) Has been cancelled
Build / Build (windows-latest) (push) Has been cancelled
Build / Analyze (javascript) (push) Has been cancelled
Build / Analyze (python) (push) Has been cancelled
462 lines
17 KiB
JavaScript
462 lines
17 KiB
JavaScript
|
|
const espresso = {};
|
|
|
|
espresso.ModelFactory = class {
|
|
|
|
async match(context) {
|
|
const identifier = context.identifier.toLowerCase();
|
|
if (identifier.endsWith('.espresso.net')) {
|
|
const obj = await context.peek('json');
|
|
if (obj && Array.isArray(obj.layers) && obj.format_version) {
|
|
return context.set('espresso.net', obj);
|
|
}
|
|
}
|
|
if (identifier.endsWith('.espresso.shape')) {
|
|
const obj = await context.peek('json');
|
|
if (obj && obj.layer_shapes) {
|
|
return context.set('espresso.shape', obj);
|
|
}
|
|
}
|
|
if (identifier.endsWith('.espresso.weights')) {
|
|
const target = await context.read('binary');
|
|
return context.set('espresso.weights', target);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
filter(context, match) {
|
|
if (context.type === 'espresso.net' && (match.type === 'espresso.weights' || match.type === 'espresso.shape' || match.type === 'coreml.metadata.mlmodelc')) {
|
|
return false;
|
|
}
|
|
if (context.type === 'espresso.shape' && (match.type === 'espresso.weights' || match.type === 'coreml.metadata.mlmodelc')) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async open(context) {
|
|
const metadata = await context.metadata('espresso-metadata.json');
|
|
switch (context.type) {
|
|
case 'espresso.net': {
|
|
const reader = new espresso.Reader(context.value, null, null);
|
|
await reader.read(context);
|
|
return new espresso.Model(metadata, reader);
|
|
}
|
|
case 'espresso.weights': {
|
|
const reader = new espresso.Reader(null, context.value, null);
|
|
await reader.read(context);
|
|
return new espresso.Model(metadata, reader);
|
|
}
|
|
case 'espresso.shape': {
|
|
const reader = new espresso.Reader(null, null, context.value);
|
|
await reader.read(context);
|
|
return new espresso.Model(metadata, reader);
|
|
}
|
|
default: {
|
|
throw new espresso.Error(`Unsupported Core ML format '${context.type}'.`);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
espresso.Model = class {
|
|
|
|
constructor(metadata, reader) {
|
|
this.format = reader.format;
|
|
this.metadata = [];
|
|
this.modules = [new espresso.Graph(metadata, reader)];
|
|
if (reader.version) {
|
|
this.version = reader.version;
|
|
}
|
|
if (reader.description) {
|
|
this.description = reader.description;
|
|
}
|
|
for (const argument of reader.properties) {
|
|
this.metadata.push(argument);
|
|
}
|
|
}
|
|
};
|
|
|
|
espresso.Graph = class {
|
|
|
|
constructor(metadata, reader) {
|
|
this.name = '';
|
|
this.type = reader.type;
|
|
for (const value of reader.values.values()) {
|
|
const name = value.name;
|
|
const type = value.type;
|
|
const description = value.description;
|
|
const initializer = value.initializer;
|
|
if (!value.value) {
|
|
value.value = new espresso.Value(name, type, description, initializer);
|
|
}
|
|
}
|
|
this.inputs = reader.inputs.map((argument) => {
|
|
const values = argument.value.map((value) => value.value);
|
|
return new espresso.Argument(argument.name, values, null, argument.visible);
|
|
});
|
|
this.outputs = reader.outputs.map((argument) => {
|
|
const values = argument.value.map((value) => value.value);
|
|
return new espresso.Argument(argument.name, values, null, argument.visible);
|
|
});
|
|
for (const obj of reader.nodes) {
|
|
const attributes = obj.attributes;
|
|
switch (obj.type) {
|
|
case 'loop':
|
|
attributes.conditionNetwork = new espresso.Graph(attributes.conditionNetwork);
|
|
attributes.bodyNetwork = new espresso.Graph(attributes.bodyNetwork);
|
|
break;
|
|
case 'branch':
|
|
attributes.ifBranch = new espresso.Graph(attributes.ifBranch);
|
|
attributes.elseBranch = new espresso.Graph(attributes.elseBranch);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
this.nodes = reader.nodes.map((obj) => new espresso.Node(metadata, obj));
|
|
}
|
|
};
|
|
|
|
espresso.Argument = class {
|
|
|
|
constructor(name, value, type = null, visible = true) {
|
|
this.name = name;
|
|
this.value = value;
|
|
this.type = type;
|
|
this.visible = visible;
|
|
}
|
|
};
|
|
|
|
espresso.Value = class {
|
|
|
|
constructor(name, type, description = null, initializer = null) {
|
|
if (typeof name !== 'string') {
|
|
throw new espresso.Error(`Invalid value identifier '${JSON.stringify(name)}'.`);
|
|
}
|
|
this.name = name;
|
|
this.type = !type && initializer ? initializer.type : type;
|
|
this.description = description;
|
|
this.initializer = initializer;
|
|
this.quantization = initializer ? initializer.quantization : null;
|
|
}
|
|
};
|
|
|
|
espresso.Node = class {
|
|
|
|
constructor(metadata, obj) {
|
|
if (!obj.type) {
|
|
throw new Error('Undefined node type.');
|
|
}
|
|
const type = metadata.type(obj.type);
|
|
this.type = type ? { ...type } : { name: obj.type };
|
|
this.type.name = obj.type.split(':').pop();
|
|
this.name = obj.name || '';
|
|
this.description = obj.description || '';
|
|
this.inputs = (obj.inputs || []).map((argument) => {
|
|
const values = argument.value.map((value) => value.value);
|
|
return new espresso.Argument(argument.name, values, null, argument.visible);
|
|
});
|
|
this.outputs = (obj.outputs || []).map((argument) => {
|
|
const values = argument.value.map((value) => value.value);
|
|
return new espresso.Argument(argument.name, values, null, argument.visible);
|
|
});
|
|
this.attributes = Object.entries(obj.attributes || []).map(([name, value]) => {
|
|
const schema = metadata.attribute(obj.type, name);
|
|
let type = null;
|
|
let visible = true;
|
|
if (schema) {
|
|
type = schema.type ? schema.type : type;
|
|
if (schema.visible === false) {
|
|
visible = false;
|
|
} else if (schema.default !== undefined) {
|
|
if (Array.isArray(value)) {
|
|
value = value.map((item) => Number(item));
|
|
}
|
|
if (typeof value === 'bigint') {
|
|
value = value.toNumber();
|
|
}
|
|
if (JSON.stringify(schema.default) === JSON.stringify(value)) {
|
|
visible = false;
|
|
}
|
|
}
|
|
}
|
|
return new espresso.Argument(name, value, type, visible);
|
|
});
|
|
if (Array.isArray(obj.chain)) {
|
|
this.chain = obj.chain.map((obj) => new espresso.Node(metadata, obj));
|
|
}
|
|
}
|
|
};
|
|
|
|
espresso.Tensor = class {
|
|
|
|
constructor(type, data, quantization, category) {
|
|
this.type = type;
|
|
this.values = data;
|
|
this.quantization = quantization;
|
|
this.category = category;
|
|
this.encoding = '<';
|
|
}
|
|
};
|
|
|
|
espresso.TensorType = class {
|
|
|
|
constructor(dataType, shape) {
|
|
this.dataType = dataType;
|
|
this.shape = shape || new espresso.TensorShape([]);
|
|
}
|
|
|
|
equals(obj) {
|
|
return obj && this.dataType === obj.dataType && this.shape && this.shape.equals(obj.shape);
|
|
}
|
|
|
|
toString() {
|
|
return this.dataType + this.shape.toString();
|
|
}
|
|
};
|
|
|
|
espresso.TensorShape = class {
|
|
|
|
constructor(dimensions) {
|
|
this.dimensions = dimensions.map((dim) => typeof dim === 'bigint' ? dim.toNumber() : dim);
|
|
}
|
|
|
|
equals(obj) {
|
|
return obj && Array.isArray(obj.dimensions) && Array.isArray(this.dimensions) &&
|
|
this.dimensions.length === obj.dimensions.length &&
|
|
obj.dimensions.every((value, index) => this.dimensions[index] === value);
|
|
}
|
|
|
|
toString() {
|
|
return Array.isArray(this.dimensions) && this.dimensions.length > 0 ?
|
|
`[${this.dimensions.map((dimension) => dimension.toString()).join(',')}]` : '';
|
|
}
|
|
};
|
|
|
|
espresso.Reader = class {
|
|
|
|
constructor(net, weights, shape) {
|
|
this.targets = [net, shape, weights];
|
|
}
|
|
|
|
async read(context) {
|
|
this.format = 'Espresso';
|
|
this.properties = [];
|
|
this.inputs = [];
|
|
this.outputs = [];
|
|
this.nodes = [];
|
|
let [net, shape, weights] = this.targets;
|
|
delete this.targets;
|
|
if (!net) {
|
|
const name = context.identifier.replace(/\.espresso\.(net|weights|shape)$/i, '.espresso.net');
|
|
const content = await context.fetch(name);
|
|
net = await content.read('json');
|
|
}
|
|
this.shapes = new Map();
|
|
if (!shape) {
|
|
const name = context.identifier.replace(/\.espresso\.(net|weights|shape)$/i, '.espresso.shape');
|
|
try {
|
|
const content = await context.fetch(name);
|
|
shape = await content.read('json');
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
}
|
|
if (shape && shape.layer_shapes) {
|
|
for (const [name, value] of Object.entries(shape.layer_shapes)) {
|
|
const dimensions = [value.n, value.k, value.w, value.h];
|
|
const shape = new espresso.TensorShape(dimensions);
|
|
this.shapes.set(name, shape);
|
|
}
|
|
}
|
|
this.blobs = new Map();
|
|
if (!weights) {
|
|
const name = net && net.storage ? net.storage : context.identifier.replace(/\.espresso\.(net|weights|shape)$/i, '.espresso.weights');
|
|
try {
|
|
const content = await context.fetch(name);
|
|
weights = await content.read('binary');
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
}
|
|
if (weights) {
|
|
const reader = weights;
|
|
const length = reader.uint64().toNumber();
|
|
for (let i = 0; i < length; i++) {
|
|
const key = reader.uint64().toNumber();
|
|
const size = reader.uint64().toNumber();
|
|
this.blobs.set(key, size);
|
|
}
|
|
for (const [key, size] of this.blobs) {
|
|
const buffer = reader.read(size);
|
|
this.blobs.set(key, buffer);
|
|
}
|
|
}
|
|
this.values = new Map();
|
|
if (net.format_version) {
|
|
const major = Math.floor(net.format_version / 100);
|
|
const minor = net.format_version % 100;
|
|
this.format += ` v${major}.${minor}`;
|
|
}
|
|
if (net && Array.isArray(net.layers)) {
|
|
for (const layer of net.layers) {
|
|
const type = layer.type;
|
|
const data = { ...layer };
|
|
const top = layer.top.split(',').map((name) => this._value(name));
|
|
const bottom = layer.bottom.split(',').map((name) => this._value(name));
|
|
const obj = {};
|
|
obj.type = type;
|
|
obj.name = layer.name;
|
|
obj.attributes = data;
|
|
obj.inputs = [{ name: 'inputs', value: bottom }];
|
|
obj.outputs = [{ name: 'outputs', value: top }];
|
|
obj.chain = [];
|
|
switch (type) {
|
|
case 'convolution':
|
|
case 'deconvolution': {
|
|
this._weights(obj, data, [data.C, data.K, data.Nx, data.Ny]);
|
|
if (data.has_biases) {
|
|
obj.inputs.push(this._initializer('biases', data.blob_biases, 'float32', [data.C]));
|
|
}
|
|
delete data.has_biases;
|
|
delete data.blob_biases;
|
|
break;
|
|
}
|
|
case 'batchnorm': {
|
|
obj.inputs.push(this._initializer('params', data.blob_batchnorm_params, 'float32', [4, data.C]));
|
|
delete data.blob_batchnorm_params;
|
|
break;
|
|
}
|
|
case 'inner_product': {
|
|
this._weights(obj, data, [data.nC, data.nB]);
|
|
if (data.has_biases) {
|
|
obj.inputs.push(this._initializer('biases', data.blob_biases, 'float32', [data.nC]));
|
|
}
|
|
delete data.has_biases;
|
|
delete data.blob_biases;
|
|
break;
|
|
}
|
|
case 'conv3d': {
|
|
this._weights(obj, data, null);
|
|
if (data.has_biases) {
|
|
obj.inputs.push(this._initializer('biases', data.blob_biases, 'float32', null));
|
|
}
|
|
delete data.has_biases;
|
|
delete data.blob_biases;
|
|
break;
|
|
}
|
|
case 'instancenorm_1d':
|
|
case 'dynamic_dequantize': {
|
|
this._weights(obj, data, null);
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
const blobs = Object.keys(data).filter((key) => key.startsWith('blob_'));
|
|
if (blobs.length > 0) {
|
|
throw new espresso.Error(`Unknown blob '${blobs.join(',')}' for type '${type}'.`);
|
|
}
|
|
if (data.has_prelu) {
|
|
obj.chain.push({ type: 'prelu' });
|
|
}
|
|
if (data.fused_relu || data.has_relu) {
|
|
obj.chain.push({ type: 'relu' });
|
|
}
|
|
if (data.fused_tanh || data.has_tanh) {
|
|
obj.chain.push({ type: 'tanh' });
|
|
}
|
|
if (data.has_batch_norm) {
|
|
obj.chain.push({ type: 'batch_norm' });
|
|
}
|
|
if (data.weights) {
|
|
for (const [name, identifier] of Object.entries(data.weights)) {
|
|
obj.inputs.push(this._initializer(name, identifier, 'float32', null));
|
|
}
|
|
delete data.weights;
|
|
}
|
|
delete data.name;
|
|
delete data.type;
|
|
delete data.top;
|
|
delete data.bottom;
|
|
delete data.fused_tanh;
|
|
delete data.fused_relu;
|
|
delete data.has_prelu;
|
|
delete data.has_relu;
|
|
delete data.has_tanh;
|
|
delete data.has_batch_norm;
|
|
this.nodes.push(obj);
|
|
}
|
|
}
|
|
delete this.shapes;
|
|
delete this.blobs;
|
|
}
|
|
|
|
_value(name) {
|
|
if (!this.values.has(name)) {
|
|
const shape = this.shapes.get(name);
|
|
const type = shape ? new espresso.TensorType('float32', shape) : null;
|
|
this.values.set(name, { name, type });
|
|
}
|
|
return this.values.get(name);
|
|
}
|
|
|
|
_weights(obj, data, dimensions) {
|
|
if (data.blob_weights !== undefined) {
|
|
obj.inputs.push(this._initializer('weights', data.blob_weights, 'float32', dimensions));
|
|
delete data.blob_weights;
|
|
return;
|
|
}
|
|
if (data.blob_weights_f16 !== undefined) {
|
|
obj.inputs.push(this._initializer('weights', data.blob_weights_f16, 'float16', dimensions));
|
|
delete data.blob_weights_f16;
|
|
return;
|
|
}
|
|
const keys = ['wBeta', 'wGamma', 'W_S8', 'W_int8', 'W_t_int8'];
|
|
for (const key of keys) {
|
|
if (data.weights && data.weights[key] !== undefined) {
|
|
let dataType = 'float32';
|
|
let name = key;
|
|
if (key.endsWith('_S8')) {
|
|
dataType = 'int8';
|
|
name = key.replace(/_S8$/, '');
|
|
} else if (key.endsWith('_int8')) {
|
|
dataType = 'int8';
|
|
name = key.replace(/_int8$/, '');
|
|
}
|
|
obj.inputs.push(this._initializer(name, data.weights[key], dataType, dimensions));
|
|
delete data.weights[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
_initializer(name, identifier, dataType, dimensions) {
|
|
if (!Number.isInteger(identifier)) {
|
|
throw new espresso.Error(`Invalid '${identifier}' blob identifier.`);
|
|
}
|
|
dataType = dataType || 'float32';
|
|
const blob = this.blobs.get(identifier);
|
|
if (!dimensions) {
|
|
const itemsize = dataType === 'float32' ? 4 : 1;
|
|
dimensions = blob ? [blob.length / itemsize] : ['?'];
|
|
}
|
|
const shape = new espresso.TensorShape(dimensions);
|
|
const type = new espresso.TensorType(dataType, shape);
|
|
const value = {};
|
|
const initializer = new espresso.Tensor(type, blob, null, 'Blob');
|
|
value.value = new espresso.Value(`${identifier}\nblob`, type, null, initializer);
|
|
return { name, value: [value] };
|
|
}
|
|
};
|
|
|
|
espresso.Error = class extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = 'Error loading Espresso model.';
|
|
}
|
|
};
|
|
|
|
export const ModelFactory = espresso.ModelFactory;
|