UNPKG

diffai-js

Version:

A Node.js wrapper for the diffai CLI tool - AI/ML specialized diff tool for PyTorch, Safetensors, NumPy, and MATLAB files with deep tensor analysis

548 lines (475 loc) 16.5 kB
/** * Node.js API wrapper for diffai CLI tool * * This module provides a JavaScript API for the diffai CLI tool, * allowing you to compare AI/ML model files programmatically. */ const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const { writeFileSync, mkdtempSync, rmSync } = require('fs'); const { tmpdir } = require('os'); /** * @typedef {'safetensors'|'pytorch'|'numpy'|'matlab'|'json'|'yaml'|'toml'|'xml'|'ini'|'csv'} Format * @typedef {'cli'|'json'|'yaml'} OutputFormat */ /** * Options for diffai operations * @typedef {Object} DiffaiOptions * @property {Format} [format] - Input file format * @property {OutputFormat} [output] - Output format * @property {boolean} [stats=false] - Show detailed tensor statistics * @property {boolean} [verbose=false] - Show verbose processing information * @property {boolean} [recursive=false] - Compare directories recursively * @property {boolean} [quantizationAnalysis=false] - Enable quantization analysis * @property {boolean} [sortByChangeMagnitude=false] - Sort changes by magnitude * @property {boolean} [showLayerImpact=false] - Show layer-wise impact analysis * @property {boolean} [architectureComparison=false] - Compare model architectures * @property {boolean} [memoryAnalysis=false] - Analyze memory usage differences * @property {boolean} [anomalyDetection=false] - Detect numerical anomalies * @property {boolean} [changeSummary=false] - Show detailed change summary * @property {boolean} [convergenceAnalysis=false] - Analyze convergence state * @property {boolean} [gradientAnalysis=false] - Analyze gradient information * @property {boolean} [similarityMatrix=false] - Generate similarity matrix * @property {boolean} [learningProgress=false] - Analyze learning progress between checkpoints * @property {boolean} [inferenceSpeedEstimate=false] - Estimate inference speed characteristics * @property {boolean} [regressionTest=false] - Perform automated regression testing * @property {boolean} [alertOnDegradation=false] - Alert on performance degradation * @property {boolean} [reviewFriendly=false] - Generate review-friendly output * @property {boolean} [deploymentReadiness=false] - Assess deployment readiness * @property {boolean} [paramEfficiencyAnalysis=false] - Analyze parameter efficiency * @property {boolean} [hyperparameterImpact=false] - Analyze hyperparameter impact * @property {boolean} [learningRateAnalysis=false] - Analyze learning rate effects * @property {boolean} [performanceImpactEstimate=false] - Estimate performance impact * @property {boolean} [generateReport=false] - Generate comprehensive analysis report * @property {boolean} [markdownOutput=false] - Output results in markdown format * @property {boolean} [includeCharts=false] - Include charts and visualizations * @property {boolean} [embeddingAnalysis=false] - Analyze embedding layer changes * @property {boolean} [attentionAnalysis=false] - Analyze attention mechanisms * @property {boolean} [headImportance=false] - Analyze attention head importance * @property {boolean} [attentionPatternDiff=false] - Compare attention patterns * @property {boolean} [clusteringChange=false] - Analyze clustering changes * @property {boolean} [hyperparameterComparison=false] - Compare hyperparameters * @property {boolean} [learningCurveAnalysis=false] - Analyze learning curves * @property {boolean} [statisticalSignificance=false] - Perform statistical significance testing * @property {string} [path] - Filter differences by path * @property {string} [ignoreKeysRegex] - Ignore keys matching regex pattern * @property {string} [arrayIdKey] - Key to use for identifying array elements * @property {number} [epsilon] - Tolerance for float comparisons */ /** * Result of a diffai operation * @typedef {Object} DiffaiResult * @property {string} type - Type of difference ('Added', 'Removed', 'Modified', 'TypeChanged') * @property {string} path - Path to the changed element * @property {*} [oldValue] - Old value (for Modified/TypeChanged) * @property {*} [newValue] - New value (for Modified/TypeChanged/Added) * @property {*} [value] - Value (for Removed) * @property {Object} [stats] - Statistical information when stats=true * @property {Object} [analysis] - Analysis results when analysis flags are enabled */ /** * Error thrown when diffai command fails */ class DiffaiError extends Error { constructor(message, exitCode, stderr) { super(message); this.name = 'DiffaiError'; this.exitCode = exitCode; this.stderr = stderr; } } /** * Get the path to the diffai binary * @returns {string} Path to diffai binary */ function getDiffaiBinaryPath() { // Determine platform-specific subdirectory const platform = process.platform; const arch = process.arch; let subdir; if (platform === 'win32') { subdir = 'win32-x64'; } else if (platform === 'darwin') { subdir = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'; } else if (platform === 'linux') { subdir = arch === 'arm64' ? 'linux-arm64' : 'linux-x64'; } else { throw new Error(`Unsupported platform: ${platform}-${arch}`); } // Check if platform-specific binary exists (OS hierarchy required) const binaryName = process.platform === 'win32' ? 'diffai.exe' : 'diffai'; const platformBinaryPath = path.join(__dirname, 'bin', subdir, binaryName); if (fs.existsSync(platformBinaryPath)) { return platformBinaryPath; } // Error if binary not found - no system PATH fallback allowed throw new Error(`Binary not found at ${platformBinaryPath}. Platform: ${platform}-${arch}. This might indicate a packaging issue. Please report this at: https://github.com/kako-jun/diffai/issues`); } /** * Execute diffai command * @param {string[]} args - Command arguments * @returns {Promise<{stdout: string, stderr: string}>} Command output */ function executeDiffai(args) { return new Promise((resolve, reject) => { const diffaiPath = getDiffaiBinaryPath(); const child = spawn(diffaiPath, args, { stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { if (code === 0 || code === 1) { // Exit code 1 means differences found, which is expected resolve({ stdout, stderr }); } else { reject(new DiffaiError( `diffai exited with code ${code}`, code, stderr )); } }); child.on('error', (err) => { if (err.code === 'ENOENT') { reject(new DiffaiError( 'diffai command not found. Please install diffai CLI tool.', -1, '' )); } else { reject(new DiffaiError(err.message, -1, '')); } }); }); } /** * Compare two AI/ML model files using diffai * * @param {string} input1 - Path to first model file * @param {string} input2 - Path to second model file * @param {DiffaiOptions} [options={}] - Comparison options * @returns {Promise<string|DiffaiResult[]>} String output for CLI format, or array of DiffaiResult for JSON format * * @example * // Basic model comparison * const result = await diff('model_v1.safetensors', 'model_v2.safetensors'); * console.log(result); * * @example * // JSON output format with statistics * const jsonResult = await diff('baseline.pt', 'finetuned.pt', { * output: 'json', * stats: true, * convergenceAnalysis: true * }); * for (const diffItem of jsonResult) { * console.log(diffItem); * } * * @example * // Advanced ML analysis * const analysis = await diff('model_old.safetensors', 'model_new.safetensors', { * output: 'json', * architectureComparison: true, * memoryAnalysis: true, * anomalyDetection: true * }); * console.log(analysis); */ async function diff(input1, input2, options = {}) { const args = [input1, input2]; // Add output format option if (options.output) { args.push('--output', options.output); } // Add format option if (options.format) { args.push('--format', options.format); } // Add basic options if (options.recursive) { args.push('--recursive'); } if (options.verbose) { args.push('--verbose'); } if (options.path) { args.push('--path', options.path); } if (options.ignoreKeysRegex) { args.push('--ignore-keys-regex', options.ignoreKeysRegex); } if (options.epsilon !== undefined) { args.push('--epsilon', options.epsilon.toString()); } if (options.arrayIdKey) { args.push('--array-id-key', options.arrayIdKey); } // Add ML analysis options if (options.showLayerImpact) { args.push('--show-layer-impact'); } if (options.quantizationAnalysis) { args.push('--quantization-analysis'); } if (options.sortByChangeMagnitude) { args.push('--sort-by-change-magnitude'); } if (options.stats) { args.push('--stats'); } if (options.learningProgress) { args.push('--learning-progress'); } if (options.convergenceAnalysis) { args.push('--convergence-analysis'); } if (options.anomalyDetection) { args.push('--anomaly-detection'); } if (options.gradientAnalysis) { args.push('--gradient-analysis'); } if (options.memoryAnalysis) { args.push('--memory-analysis'); } if (options.inferenceSpeedEstimate) { args.push('--inference-speed-estimate'); } if (options.regressionTest) { args.push('--regression-test'); } if (options.alertOnDegradation) { args.push('--alert-on-degradation'); } if (options.reviewFriendly) { args.push('--review-friendly'); } if (options.changeSummary) { args.push('--change-summary'); } if (options.deploymentReadiness) { args.push('--deployment-readiness'); } if (options.architectureComparison) { args.push('--architecture-comparison'); } if (options.paramEfficiencyAnalysis) { args.push('--param-efficiency-analysis'); } if (options.hyperparameterImpact) { args.push('--hyperparameter-impact'); } if (options.learningRateAnalysis) { args.push('--learning-rate-analysis'); } if (options.performanceImpactEstimate) { args.push('--performance-impact-estimate'); } if (options.generateReport) { args.push('--generate-report'); } if (options.markdownOutput) { args.push('--markdown-output'); } if (options.includeCharts) { args.push('--include-charts'); } if (options.embeddingAnalysis) { args.push('--embedding-analysis'); } if (options.similarityMatrix) { args.push('--similarity-matrix'); } if (options.clusteringChange) { args.push('--clustering-change'); } if (options.attentionAnalysis) { args.push('--attention-analysis'); } if (options.headImportance) { args.push('--head-importance'); } if (options.attentionPatternDiff) { args.push('--attention-pattern-diff'); } if (options.hyperparameterComparison) { args.push('--hyperparameter-comparison'); } if (options.learningCurveAnalysis) { args.push('--learning-curve-analysis'); } if (options.statisticalSignificance) { args.push('--statistical-significance'); } const { stdout, stderr } = await executeDiffai(args); // If output format is JSON, parse the result if (options.output === 'json') { try { const jsonData = JSON.parse(stdout); return jsonData.map(item => { if (item.Added) { return { type: 'Added', path: item.Added[0], newValue: item.Added[1] }; } else if (item.Removed) { return { type: 'Removed', path: item.Removed[0], value: item.Removed[1] }; } else if (item.Modified) { return { type: 'Modified', path: item.Modified[0], oldValue: item.Modified[1], newValue: item.Modified[2] }; } else if (item.TypeChanged) { return { type: 'TypeChanged', path: item.TypeChanged[0], oldValue: item.TypeChanged[1], newValue: item.TypeChanged[2] }; } return item; }); } catch (e) { throw new DiffaiError(`Failed to parse JSON output: ${e.message}`, -1, ''); } } // Return raw output for other formats return stdout; } /** * Compare two model content strings directly (writes to temporary files) * * @param {string} content1 - First model content (base64 or binary string) * @param {string} content2 - Second model content (base64 or binary string) * @param {Format} format - Content format * @param {DiffaiOptions} [options={}] - Comparison options * @returns {Promise<string|DiffaiResult[]>} String output for CLI format, or array of DiffaiResult for JSON format * * @example * const modelData1 = fs.readFileSync('model1.safetensors'); * const modelData2 = fs.readFileSync('model2.safetensors'); * const result = await diffString(modelData1, modelData2, 'safetensors', { * output: 'json', * stats: true * }); * console.log(result); */ async function diffString(content1, content2, format, options = {}) { // Ensure format is set options.format = format; // Create temporary files const tmpDir = mkdtempSync(path.join(tmpdir(), 'diffai-')); const extension = format === 'pytorch' ? 'pt' : format === 'numpy' ? 'npy' : format === 'matlab' ? 'mat' : format; const tmpFile1 = path.join(tmpDir, `file1.${extension}`); const tmpFile2 = path.join(tmpDir, `file2.${extension}`); try { // Write content to temporary files if (typeof content1 === 'string') { writeFileSync(tmpFile1, content1, 'utf8'); writeFileSync(tmpFile2, content2, 'utf8'); } else { // Handle binary content writeFileSync(tmpFile1, content1); writeFileSync(tmpFile2, content2); } // Perform diff return await diff(tmpFile1, tmpFile2, options); } finally { // Clean up temporary files rmSync(tmpDir, { recursive: true, force: true }); } } /** * Analyze a single model file (inspection mode) * * @param {string} modelPath - Path to the model file * @param {DiffaiOptions} [options={}] - Analysis options * @returns {Promise<string|Object>} Analysis result * * @example * const analysis = await inspect('model.safetensors', { * output: 'json', * stats: true, * memoryAnalysis: true * }); * console.log(analysis); */ async function inspect(modelPath, options = {}) { // Create a dummy empty file for comparison to enable inspection mode const tmpDir = mkdtempSync(path.join(tmpdir(), 'diffai-inspect-')); const extension = path.extname(modelPath).slice(1) || 'bin'; const emptyFile = path.join(tmpDir, `empty.${extension}`); try { // Create minimal empty file writeFileSync(emptyFile, ''); // Use diff with special handling for inspection const result = await diff(emptyFile, modelPath, options); // For inspection mode, we're mainly interested in the "Added" items // which represent the structure of the single file return result; } finally { rmSync(tmpDir, { recursive: true, force: true }); } } /** * Check if diffai command is available in the system * * @returns {Promise<boolean>} True if diffai is available, false otherwise * * @example * if (!(await isDiffaiAvailable())) { * console.error('Please install diffai CLI tool'); * process.exit(1); * } */ async function isDiffaiAvailable() { try { await executeDiffai(['--version']); return true; } catch (err) { return false; } } /** * Get version information of diffai * * @returns {Promise<string>} Version string * * @example * const version = await getVersion(); * console.log(`diffai version: ${version}`); */ async function getVersion() { try { const { stdout } = await executeDiffai(['--version']); return stdout.trim(); } catch (err) { throw new DiffaiError('Failed to get version information', -1, ''); } } module.exports = { diff, diffString, inspect, isDiffaiAvailable, getVersion, DiffaiError };