mirror of
https://github.com/signerless/llm-checker.git
synced 2026-10-19 09:54:45 +00:00
- requirements.parseModelSize globally stripped non-[0-9.kmb] chars, so stray
letters in model words corrupted the size ("Llama 3.2 3B" -> 0.003B) and
unit-only inputs went NaN. Now anchors the number to a real B/M/K unit and
prefers the size token over a version number.
- expanded_database.estimateMemoryUsage/estimatePowerConsumption stripped the
unit and treated the bare number as gigabytes, so "774M" reported ~929 GB and
"22M" ~26 GB. Derive footprint from parameter count instead (~1 GB for both now).
- compatibility.parseModelSize threw on undefined/numeric size (runs per model);
added the same type guard performance.js already had.
- apple-silicon.getFingerprint crashed on null.chip when the sysctl brand-string
read failed; fall back to 'apple-silicon'.
- intel VRAM: stop letting the PCI MMIO BAR size (not actual VRAM) shadow the
reliable model-based estimate — prefer estimateVRAM, BAR only as last resort.
- rocm: when a multi-SKU lspci bracket has an unknown device id, keep the full
variant list instead of confidently picking the lowest-tier SKU's (wrong) VRAM.
- performance.estimateLoadTime: folded a leftover `* 2` then `* 0.7` two-step
(with a dead blank line) into one documented factor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
452 lines
16 KiB
JavaScript
452 lines
16 KiB
JavaScript
const { estimateTokenSpeedFromHardware } = require('../src/utils/token-speed-estimator');
|
|
|
|
class PerformanceAnalyzer {
|
|
constructor() {
|
|
this.benchmarkCache = new Map();
|
|
this.cacheExpiry = 24 * 60 * 60 * 1000; // 24 horas
|
|
}
|
|
|
|
async analyzeSystemPerformance(hardware) {
|
|
const cacheKey = this.generateCacheKey(hardware);
|
|
const cached = this.benchmarkCache.get(cacheKey);
|
|
|
|
if (cached && Date.now() - cached.timestamp < this.cacheExpiry) {
|
|
return cached.data;
|
|
}
|
|
|
|
const performance = {
|
|
cpu: await this.analyzeCPUPerformance(hardware.cpu),
|
|
memory: await this.analyzeMemoryPerformance(hardware.memory),
|
|
gpu: await this.analyzeGPUPerformance(hardware.gpu),
|
|
storage: await this.analyzeStoragePerformance(),
|
|
overall: 0
|
|
};
|
|
|
|
// Calculate overall score
|
|
performance.overall = Math.round(
|
|
(performance.cpu.score * 0.3 +
|
|
performance.memory.score * 0.25 +
|
|
performance.gpu.score * 0.35 +
|
|
performance.storage.score * 0.1)
|
|
);
|
|
|
|
// Cache results
|
|
this.benchmarkCache.set(cacheKey, {
|
|
data: performance,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
return performance;
|
|
}
|
|
|
|
async analyzeCPUPerformance(cpu) {
|
|
const analysis = {
|
|
score: cpu.score || 50,
|
|
details: {
|
|
cores: cpu.cores,
|
|
threads: cpu.physicalCores * 2, // Assume hyperthreading
|
|
speed: cpu.speedMax || cpu.speed,
|
|
architecture: cpu.architecture,
|
|
cache: cpu.cache
|
|
},
|
|
benchmarks: {},
|
|
strengths: [],
|
|
weaknesses: [],
|
|
recommendations: []
|
|
};
|
|
|
|
// Run CPU benchmarks
|
|
try {
|
|
analysis.benchmarks = await this.runCPUBenchmarks();
|
|
} catch (error) {
|
|
analysis.benchmarks = { error: error.message };
|
|
}
|
|
|
|
// Analyze strengths and weaknesses
|
|
this.analyzeCPUCharacteristics(analysis);
|
|
|
|
return analysis;
|
|
}
|
|
|
|
async runCPUBenchmarks() {
|
|
const results = {};
|
|
|
|
// Single-threaded performance test
|
|
const singleThreadStart = process.hrtime.bigint();
|
|
let result = 0;
|
|
for (let i = 0; i < 1000000; i++) {
|
|
result += Math.sqrt(i) * Math.sin(i);
|
|
}
|
|
const singleThreadEnd = process.hrtime.bigint();
|
|
results.singleThread = {
|
|
duration: Number(singleThreadEnd - singleThreadStart) / 1000000, // ms
|
|
score: Math.max(0, 1000 - (Number(singleThreadEnd - singleThreadStart) / 1000000))
|
|
};
|
|
|
|
// Multi-threaded performance test (simulate with Promise.all)
|
|
const multiThreadStart = process.hrtime.bigint();
|
|
const workers = Array.from({ length: 4 }, () =>
|
|
Promise.resolve().then(() => {
|
|
let result = 0;
|
|
for (let i = 0; i < 250000; i++) {
|
|
result += Math.sqrt(i) * Math.sin(i);
|
|
}
|
|
return result;
|
|
})
|
|
);
|
|
await Promise.all(workers);
|
|
const multiThreadEnd = process.hrtime.bigint();
|
|
results.multiThread = {
|
|
duration: Number(multiThreadEnd - multiThreadStart) / 1000000, // ms
|
|
score: Math.max(0, 1000 - (Number(multiThreadEnd - multiThreadStart) / 1000000))
|
|
};
|
|
|
|
// Memory bandwidth test
|
|
const memBandwidthStart = process.hrtime.bigint();
|
|
const largeArray = new Array(1000000).fill(0);
|
|
for (let i = 0; i < largeArray.length; i++) {
|
|
largeArray[i] = Math.random();
|
|
}
|
|
largeArray.sort();
|
|
const memBandwidthEnd = process.hrtime.bigint();
|
|
results.memoryBandwidth = {
|
|
duration: Number(memBandwidthEnd - memBandwidthStart) / 1000000, // ms
|
|
score: Math.max(0, 2000 - (Number(memBandwidthEnd - memBandwidthStart) / 1000000))
|
|
};
|
|
|
|
return results;
|
|
}
|
|
|
|
analyzeCPUCharacteristics(analysis) {
|
|
const { details, benchmarks } = analysis;
|
|
|
|
// Strengths
|
|
if (details.cores >= 8) {
|
|
analysis.strengths.push('High core count suitable for parallel processing');
|
|
}
|
|
if (details.speed >= 3.5) {
|
|
analysis.strengths.push('High clock speed for single-threaded performance');
|
|
}
|
|
if (details.architecture === 'Apple Silicon') {
|
|
analysis.strengths.push('Unified memory architecture with excellent efficiency');
|
|
}
|
|
if (details.cache.l3 >= 16) {
|
|
analysis.strengths.push('Large L3 cache improves model loading performance');
|
|
}
|
|
|
|
// Weaknesses
|
|
if (details.cores < 4) {
|
|
analysis.weaknesses.push('Low core count may limit concurrent model execution');
|
|
}
|
|
if (details.speed < 2.5) {
|
|
analysis.weaknesses.push('Low clock speed may impact inference speed');
|
|
}
|
|
if (benchmarks.singleThread?.score < 500) {
|
|
analysis.weaknesses.push('Below-average single-threaded performance');
|
|
}
|
|
|
|
// Recommendations
|
|
if (details.cores >= 8) {
|
|
analysis.recommendations.push('Consider running multiple small models simultaneously');
|
|
}
|
|
if (details.architecture === 'Apple Silicon') {
|
|
analysis.recommendations.push('Use llama.cpp with Metal acceleration for optimal performance');
|
|
}
|
|
if (details.cores < 6) {
|
|
analysis.recommendations.push('Focus on smaller models (1B-7B parameters)');
|
|
}
|
|
}
|
|
|
|
async analyzeMemoryPerformance(memory) {
|
|
const analysis = {
|
|
score: memory.score || 50,
|
|
details: {
|
|
total: memory.total,
|
|
available: memory.free,
|
|
usage: memory.usagePercent,
|
|
type: 'Unknown' // Would need additional detection
|
|
},
|
|
characteristics: {},
|
|
recommendations: []
|
|
};
|
|
|
|
// Memory characteristics analysis
|
|
this.analyzeMemoryCharacteristics(analysis);
|
|
|
|
return analysis;
|
|
}
|
|
|
|
analyzeMemoryCharacteristics(analysis) {
|
|
const { details } = analysis;
|
|
|
|
// Memory adequacy for different model sizes
|
|
analysis.characteristics = {
|
|
ultraSmall: details.total >= 2,
|
|
small: details.total >= 8,
|
|
medium: details.total >= 16,
|
|
large: details.total >= 32,
|
|
ultraLarge: details.total >= 64
|
|
};
|
|
|
|
// Recommendations based on memory
|
|
if (details.total < 8) {
|
|
analysis.recommendations.push('Upgrade to 16GB+ RAM for better model compatibility');
|
|
analysis.recommendations.push('Use aggressive quantization (Q2_K, Q3_K_M)');
|
|
} else if (details.total < 16) {
|
|
analysis.recommendations.push('Current RAM suitable for small-medium models');
|
|
analysis.recommendations.push('Consider 32GB for large model flexibility');
|
|
} else if (details.total >= 32) {
|
|
analysis.recommendations.push('Excellent RAM capacity for most models');
|
|
analysis.recommendations.push('Can run multiple models simultaneously');
|
|
}
|
|
|
|
if (details.usage > 80) {
|
|
analysis.recommendations.push('High memory usage - close unnecessary applications');
|
|
analysis.recommendations.push('Consider memory optimization tools');
|
|
}
|
|
}
|
|
|
|
async analyzeGPUPerformance(gpu) {
|
|
const analysis = {
|
|
score: gpu.score || 0,
|
|
details: {
|
|
model: gpu.model,
|
|
vram: gpu.vram,
|
|
dedicated: gpu.dedicated,
|
|
vendor: gpu.vendor
|
|
},
|
|
capabilities: {},
|
|
recommendations: []
|
|
};
|
|
|
|
// GPU capabilities analysis
|
|
this.analyzeGPUCapabilities(analysis);
|
|
|
|
return analysis;
|
|
}
|
|
|
|
analyzeGPUCapabilities(analysis) {
|
|
const { details } = analysis;
|
|
|
|
// VRAM adequacy for different model sizes
|
|
analysis.capabilities = {
|
|
acceleratesSmall: details.vram >= 4 && details.dedicated,
|
|
acceleratesMedium: details.vram >= 8 && details.dedicated,
|
|
acceleratesLarge: details.vram >= 16 && details.dedicated,
|
|
acceleratesUltraLarge: details.vram >= 24 && details.dedicated
|
|
};
|
|
|
|
// GPU-specific recommendations
|
|
if (!details.dedicated) {
|
|
analysis.recommendations.push('Integrated GPU detected - CPU inference recommended');
|
|
analysis.recommendations.push('Consider dedicated GPU for significant speedup');
|
|
} else if (details.vram < 4) {
|
|
analysis.recommendations.push('Limited VRAM - focus on CPU inference or small models');
|
|
} else if (details.vram >= 8) {
|
|
analysis.recommendations.push('Good VRAM capacity for GPU-accelerated inference');
|
|
analysis.recommendations.push('Enable GPU acceleration in llama.cpp or Ollama');
|
|
}
|
|
|
|
if (details.vendor === 'NVIDIA' && details.dedicated) {
|
|
analysis.recommendations.push('NVIDIA GPU detected - CUDA acceleration available');
|
|
} else if (details.vendor === 'AMD' && details.dedicated) {
|
|
analysis.recommendations.push('AMD GPU detected - ROCm acceleration may be available');
|
|
}
|
|
}
|
|
|
|
async analyzeStoragePerformance() {
|
|
const analysis = {
|
|
score: 70, // Default assumption of SSD
|
|
details: {
|
|
type: 'Unknown',
|
|
estimatedSpeed: 'Unknown'
|
|
},
|
|
impact: {},
|
|
recommendations: []
|
|
};
|
|
|
|
// Storage impact on model loading
|
|
analysis.impact = {
|
|
modelLoadTime: 'Moderate', // Would be faster with NVMe
|
|
swapPerformance: 'Adequate',
|
|
tempFileAccess: 'Good'
|
|
};
|
|
|
|
// Storage recommendations
|
|
analysis.recommendations.push('SSD storage recommended for faster model loading');
|
|
analysis.recommendations.push('NVMe storage provides best performance for large models');
|
|
analysis.recommendations.push('Ensure sufficient free space for model downloads');
|
|
|
|
return analysis;
|
|
}
|
|
|
|
async estimateModelPerformance(model, hardware) {
|
|
// Use realistic estimation that considers hardware type properly
|
|
return this.calculateRealisticPerformance(model, hardware);
|
|
}
|
|
|
|
calculateRealisticPerformance(model, hardware) {
|
|
const modelSizeB = this.parseModelSize(model.size);
|
|
const speedProfile = estimateTokenSpeedFromHardware(hardware, {
|
|
modelSizeB,
|
|
modelName: model.name
|
|
});
|
|
const tokensPerSecond = speedProfile.tokensPerSecond;
|
|
|
|
return {
|
|
estimatedTokensPerSecond: tokensPerSecond,
|
|
confidence: this.calculateConfidence(hardware, model),
|
|
factors: {
|
|
cpu: hardware.cpu?.brand || hardware.cpu?.model || 'Unknown CPU',
|
|
memory: hardware.memory?.total || 0,
|
|
gpu: speedProfile.backend,
|
|
modelSize: modelSizeB,
|
|
architecture: hardware.cpu?.architecture || 'unknown',
|
|
sizeScale: speedProfile.sizeScale,
|
|
memoryFactor: speedProfile.memoryFactor
|
|
},
|
|
category: this.categorizePerformance(tokensPerSecond),
|
|
loadTimeEstimate: this.estimateLoadTime(model, hardware)
|
|
};
|
|
}
|
|
|
|
parseModelSize(sizeString) {
|
|
if (typeof sizeString !== 'string' || !sizeString.trim()) return 1;
|
|
|
|
const normalized = sizeString.trim().toUpperCase();
|
|
|
|
// Parameter notation (e.g. 8B, 774M)
|
|
const paramMatch = normalized.match(/(\d+\.?\d*)\s*([BM])\b/);
|
|
if (paramMatch) {
|
|
const num = parseFloat(paramMatch[1]);
|
|
const unit = paramMatch[2];
|
|
return unit === 'B' ? num : num / 1000;
|
|
}
|
|
|
|
// File-size notation fallback (e.g. 4.9GB) -> rough Q4 param estimate
|
|
const gbMatch = normalized.match(/(\d+\.?\d*)\s*GB\b/);
|
|
if (gbMatch) {
|
|
const sizeGB = parseFloat(gbMatch[1]);
|
|
return Math.max(0.5, sizeGB / 0.62);
|
|
}
|
|
|
|
const mbMatch = normalized.match(/(\d+\.?\d*)\s*MB\b/);
|
|
if (mbMatch) {
|
|
const sizeGB = parseFloat(mbMatch[1]) / 1024;
|
|
return Math.max(0.5, sizeGB / 0.62);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
calculateConfidence(hardware, model) {
|
|
let confidence = 50; // Base confidence
|
|
|
|
// Higher confidence for better documented hardware
|
|
if (hardware.cpu.score > 70) confidence += 20;
|
|
if (hardware.memory.total >= 16) confidence += 15;
|
|
if (hardware.gpu.dedicated) confidence += 10;
|
|
|
|
// Lower confidence for edge cases
|
|
if (hardware.memory.total < 4) confidence -= 30;
|
|
if (!model.requirements) confidence -= 20;
|
|
|
|
return Math.max(10, Math.min(90, confidence));
|
|
}
|
|
|
|
categorizePerformance(tokensPerSecond) {
|
|
if (tokensPerSecond >= 50) return 'excellent';
|
|
if (tokensPerSecond >= 25) return 'good';
|
|
if (tokensPerSecond >= 10) return 'moderate';
|
|
if (tokensPerSecond >= 5) return 'slow';
|
|
return 'very_slow';
|
|
}
|
|
|
|
estimateLoadTime(model, hardware) {
|
|
// ~2 GB per 1B params (fp16-ish) on-disk approximation.
|
|
const modelSizeGB = this.parseModelSize(model.size) * 2;
|
|
|
|
// Fold the previous `* 2` then `* 0.7` two-step (a leftover from an
|
|
// incomplete edit, with a dead blank line) into one documented factor:
|
|
// ~1.4 s of load time per GB before hardware adjustments.
|
|
let loadTimeSeconds = modelSizeGB * 1.4;
|
|
|
|
const cpuSpeedFactor = Math.max(0.5, Math.min(1.5, (hardware.cpu.speed || 2.5) / 2.5));
|
|
loadTimeSeconds /= cpuSpeedFactor;
|
|
|
|
|
|
// Load time doesn't depend on current free memory, but on total RAM vs model size
|
|
if (hardware.memory.total < modelSizeGB * 1.5) {
|
|
loadTimeSeconds *= 1.5; // Slower if tight on total memory
|
|
}
|
|
|
|
return {
|
|
estimated: Math.round(loadTimeSeconds),
|
|
confidence: this.calculateConfidence(hardware, model),
|
|
factors: ['storage_speed', 'cpu_performance', 'available_memory']
|
|
};
|
|
}
|
|
|
|
generateCacheKey(hardware) {
|
|
return `${hardware.cpu.brand}-${hardware.memory.total}-${hardware.gpu.model}`;
|
|
}
|
|
|
|
async benchmarkInferenceSpeed(modelName, hardware, ollamaClient) {
|
|
if (!ollamaClient) {
|
|
throw new Error('Ollama client required for inference benchmarking');
|
|
}
|
|
|
|
const testPrompts = [
|
|
"Hello, how are you today?",
|
|
"Explain the concept of artificial intelligence in one sentence.",
|
|
"What is 2 + 2?",
|
|
"Write a haiku about programming.",
|
|
"List three benefits of renewable energy."
|
|
];
|
|
|
|
const results = [];
|
|
|
|
for (const prompt of testPrompts) {
|
|
try {
|
|
const result = await ollamaClient.testModelPerformance(modelName, prompt);
|
|
results.push({
|
|
prompt: prompt.substring(0, 30) + '...',
|
|
tokensPerSecond: result.tokensPerSecond,
|
|
responseTime: result.responseTime,
|
|
success: result.success
|
|
});
|
|
} catch (error) {
|
|
results.push({
|
|
prompt: prompt.substring(0, 30) + '...',
|
|
tokensPerSecond: 0,
|
|
responseTime: 0,
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
const successful = results.filter(r => r.success);
|
|
const avgTokensPerSecond = successful.length > 0 ?
|
|
successful.reduce((sum, r) => sum + r.tokensPerSecond, 0) / successful.length : 0;
|
|
|
|
return {
|
|
model: modelName,
|
|
hardware: {
|
|
cpu: hardware.cpu.brand,
|
|
ram: hardware.memory.total,
|
|
gpu: hardware.gpu.model
|
|
},
|
|
averageTokensPerSecond: Math.round(avgTokensPerSecond * 10) / 10,
|
|
successRate: (successful.length / results.length) * 100,
|
|
detailedResults: results,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
clearCache() {
|
|
this.benchmarkCache.clear();
|
|
}
|
|
}
|
|
|
|
module.exports = PerformanceAnalyzer;
|