llm-checker 3.5.3 → 3.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/bin/enhanced_cli.js +9 -2
- package/package.json +1 -1
- package/src/hardware/backends/rocm-detector.js +52 -6
- package/src/hardware/detector.js +34 -11
- package/src/hardware/unified-detector.js +94 -1
- package/src/models/ai-check-selector.js +6 -0
- package/src/models/fine-tuning-support.js +215 -0
package/README.md
CHANGED
|
@@ -531,19 +531,24 @@ Hardware Tier: HIGH | Models Analyzed: 205
|
|
|
531
531
|
Coding:
|
|
532
532
|
qwen2.5-coder:14b (14B)
|
|
533
533
|
Score: 78/100
|
|
534
|
+
Fine-tuning: LoRA+QLoRA
|
|
534
535
|
Command: ollama pull qwen2.5-coder:14b
|
|
535
536
|
|
|
536
537
|
Reasoning:
|
|
537
538
|
deepseek-r1:14b (14B)
|
|
538
539
|
Score: 86/100
|
|
540
|
+
Fine-tuning: QLoRA
|
|
539
541
|
Command: ollama pull deepseek-r1:14b
|
|
540
542
|
|
|
541
543
|
Multimodal:
|
|
542
544
|
llama3.2-vision:11b (11B)
|
|
543
545
|
Score: 83/100
|
|
546
|
+
Fine-tuning: LoRA+QLoRA
|
|
544
547
|
Command: ollama pull llama3.2-vision:11b
|
|
545
548
|
```
|
|
546
549
|
|
|
550
|
+
`check`, `recommend`, and `ai-check` include a fine-tuning suitability label in output to help choose between Full FT, LoRA, and QLoRA paths.
|
|
551
|
+
|
|
547
552
|
### `search` — Model Search
|
|
548
553
|
|
|
549
554
|
```bash
|
package/bin/enhanced_cli.js
CHANGED
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
getRuntimeDisplayName,
|
|
24
24
|
getRuntimeCommandSet
|
|
25
25
|
} = require('../src/runtime/runtime-support');
|
|
26
|
+
const { evaluateFineTuningSupport } = require('../src/models/fine-tuning-support');
|
|
26
27
|
const { CalibrationManager } = require('../src/calibration/calibration-manager');
|
|
27
28
|
const { SUPPORTED_CALIBRATION_OBJECTIVES } = require('../src/calibration/schemas');
|
|
28
29
|
const {
|
|
@@ -1203,7 +1204,7 @@ function displayLegacyRecommendations(recommendations) {
|
|
|
1203
1204
|
console.log(chalk.cyan('╰'));
|
|
1204
1205
|
}
|
|
1205
1206
|
|
|
1206
|
-
function displayIntelligentRecommendations(intelligentData) {
|
|
1207
|
+
function displayIntelligentRecommendations(intelligentData, hardware = null) {
|
|
1207
1208
|
if (!intelligentData || !intelligentData.summary) return;
|
|
1208
1209
|
|
|
1209
1210
|
const { summary, recommendations } = intelligentData;
|
|
@@ -1220,10 +1221,12 @@ function displayIntelligentRecommendations(intelligentData) {
|
|
|
1220
1221
|
// Mostrar mejor modelo general
|
|
1221
1222
|
if (summary.best_overall) {
|
|
1222
1223
|
const best = summary.best_overall;
|
|
1224
|
+
const bestFineTuning = evaluateFineTuningSupport(best, hardware || {});
|
|
1223
1225
|
console.log(chalk.red('│') + ` ${chalk.bold.yellow('BEST OVERALL:')} ${chalk.green.bold(best.name)}`);
|
|
1224
1226
|
console.log(chalk.red('│') + ` Command: ${chalk.cyan.bold(best.command)}`);
|
|
1225
1227
|
console.log(chalk.red('│') + ` Score: ${chalk.yellow.bold(best.score)}/100 | Category: ${chalk.magenta(best.category)}`);
|
|
1226
1228
|
console.log(chalk.red('│') + ` Quantization: ${chalk.white.bold(best.quantization || 'Q4_K_M')}`);
|
|
1229
|
+
console.log(chalk.red('│') + ` Fine-tuning: ${chalk.blue.bold(bestFineTuning.shortLabel)}`);
|
|
1227
1230
|
console.log(chalk.red('│'));
|
|
1228
1231
|
}
|
|
1229
1232
|
|
|
@@ -1242,11 +1245,13 @@ function displayIntelligentRecommendations(intelligentData) {
|
|
|
1242
1245
|
const icon = categories[category] || 'Other';
|
|
1243
1246
|
const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
|
|
1244
1247
|
const scoreColor = getScoreColor(model.score);
|
|
1248
|
+
const fineTuningSupport = evaluateFineTuningSupport(model, hardware || {});
|
|
1245
1249
|
|
|
1246
1250
|
console.log(chalk.red('│') + ` ${chalk.bold.white(categoryName)} (${icon}):`);
|
|
1247
1251
|
console.log(chalk.red('│') + ` ${chalk.green(model.name)} (${model.size})`);
|
|
1248
1252
|
console.log(chalk.red('│') + ` Score: ${scoreColor.bold(model.score)}/100 | Pulls: ${chalk.gray(model.pulls?.toLocaleString() || 'N/A')}`);
|
|
1249
1253
|
console.log(chalk.red('│') + ` Quantization: ${chalk.white.bold(model.quantization || 'Q4_K_M')}`);
|
|
1254
|
+
console.log(chalk.red('│') + ` Fine-tuning: ${chalk.blue.bold(fineTuningSupport.shortLabel)}`);
|
|
1250
1255
|
console.log(chalk.red('│') + ` Command: ${chalk.cyan.bold(model.command)}`);
|
|
1251
1256
|
console.log(chalk.red('│'));
|
|
1252
1257
|
});
|
|
@@ -2079,6 +2084,8 @@ async function displayModelRecommendations(analysis, hardware, useCase = 'genera
|
|
|
2079
2084
|
const realSize = getRealSizeFromOllamaCache(model) || estimateModelSize(model);
|
|
2080
2085
|
console.log(`Size: ${chalk.white(realSize)}`);
|
|
2081
2086
|
console.log(`Compatibility Score: ${chalk.green.bold(model.adjustedScore || model.score || 'N/A')}/100`);
|
|
2087
|
+
const fineTuningSupport = evaluateFineTuningSupport(model, hardware);
|
|
2088
|
+
console.log(`Fine-tuning: ${chalk.blue.bold(fineTuningSupport.shortLabel)}`);
|
|
2082
2089
|
|
|
2083
2090
|
if (index === 0) {
|
|
2084
2091
|
console.log(`Reason: ${chalk.gray(reason)}`);
|
|
@@ -3628,7 +3635,7 @@ Calibrated routing examples:
|
|
|
3628
3635
|
displaySystemInfo(hardware, { summary: { hardwareTier: intelligentRecommendations.summary.hardware_tier } });
|
|
3629
3636
|
|
|
3630
3637
|
// Mostrar recomendaciones
|
|
3631
|
-
displayIntelligentRecommendations(intelligentRecommendations);
|
|
3638
|
+
displayIntelligentRecommendations(intelligentRecommendations, hardware);
|
|
3632
3639
|
displayCalibratedRoutingDecision('recommend', calibratedPolicy, routeDecision, routingPreference.warnings);
|
|
3633
3640
|
|
|
3634
3641
|
if (policyConfig && policyEvaluation && policyEnforcement) {
|
package/package.json
CHANGED
|
@@ -248,13 +248,20 @@ class ROCmDetector {
|
|
|
248
248
|
timeout: 10000
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
// Parse memory info
|
|
252
|
-
|
|
251
|
+
// Parse memory info. Newer rocm-smi reports bytes "(B)" while some
|
|
252
|
+
// systems expose MiB; normalize to GB safely.
|
|
253
253
|
const gpuMemory = {};
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
254
|
+
const memLines = String(memInfo || '').split('\n');
|
|
255
|
+
for (const line of memLines) {
|
|
256
|
+
const lineMatch = line.match(/GPU\[(\d+)\].*?Total.*?Memory\s*(?:\(([^)]+)\))?\s*:\s*(\d+)/i);
|
|
257
|
+
if (!lineMatch) continue;
|
|
258
|
+
|
|
259
|
+
const idx = parseInt(lineMatch[1], 10);
|
|
260
|
+
const unitHint = lineMatch[2] || '';
|
|
261
|
+
const rawValue = parseInt(lineMatch[3], 10);
|
|
262
|
+
|
|
263
|
+
if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
|
|
264
|
+
gpuMemory[idx] = this.normalizeRocmMemoryToGB(rawValue, unitHint);
|
|
258
265
|
}
|
|
259
266
|
|
|
260
267
|
// Get temperature and utilization
|
|
@@ -312,6 +319,45 @@ class ROCmDetector {
|
|
|
312
319
|
}
|
|
313
320
|
}
|
|
314
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Normalize rocm-smi memory values to GB.
|
|
324
|
+
* rocm-smi may report bytes "(B)" or MiB depending on version/system.
|
|
325
|
+
*/
|
|
326
|
+
normalizeRocmMemoryToGB(value, unitHint = '') {
|
|
327
|
+
const numericValue = Number(value);
|
|
328
|
+
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const unit = String(unitHint || '').toLowerCase();
|
|
333
|
+
|
|
334
|
+
if (unit.includes('gib') || unit.includes('gb')) {
|
|
335
|
+
return Math.round(numericValue);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (unit.includes('mib') || unit.includes('mb')) {
|
|
339
|
+
return Math.round(numericValue / 1024);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (unit.includes('kib') || unit.includes('kb')) {
|
|
343
|
+
return Math.round(numericValue / (1024 * 1024));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (unit === 'b' || unit === 'bytes') {
|
|
347
|
+
return Math.round(numericValue / (1024 ** 3));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Unit was not provided. Use value magnitude heuristics.
|
|
351
|
+
if (numericValue >= 1024 ** 3) {
|
|
352
|
+
return Math.round(numericValue / (1024 ** 3));
|
|
353
|
+
}
|
|
354
|
+
if (numericValue >= 1024) {
|
|
355
|
+
return Math.round(numericValue / 1024);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return Math.round(numericValue);
|
|
359
|
+
}
|
|
360
|
+
|
|
315
361
|
/**
|
|
316
362
|
* Detect GPUs via rocminfo
|
|
317
363
|
*/
|
package/src/hardware/detector.js
CHANGED
|
@@ -255,20 +255,43 @@ class HardwareDetector {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
const primaryType = unified.primary.type || 'cpu';
|
|
258
|
-
|
|
258
|
+
const hasFallbackDedicatedGpu = Boolean(
|
|
259
|
+
primaryType === 'cpu' &&
|
|
260
|
+
unified.systemGpu?.available &&
|
|
261
|
+
Array.isArray(unified.systemGpu.gpus) &&
|
|
262
|
+
unified.systemGpu.gpus.some((gpu) => gpu.type === 'dedicated')
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (primaryType === 'cpu' && !hasFallbackDedicatedGpu) {
|
|
259
266
|
return;
|
|
260
267
|
}
|
|
261
268
|
|
|
262
269
|
const summary = unified.summary;
|
|
263
|
-
const backendInfo =
|
|
264
|
-
|
|
265
|
-
|
|
270
|
+
const backendInfo = hasFallbackDedicatedGpu
|
|
271
|
+
? unified.systemGpu
|
|
272
|
+
: (unified.backends?.[primaryType]?.info || {});
|
|
266
273
|
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
const backendGPUs = Array.isArray(backendInfo.gpus) ? backendInfo.gpus : [];
|
|
275
|
+
const dedicatedBackendGPUs = backendGPUs.filter((gpu) => gpu?.type !== 'integrated');
|
|
276
|
+
|
|
277
|
+
const gpuCount = summary.gpuCount ||
|
|
278
|
+
dedicatedBackendGPUs.length ||
|
|
279
|
+
backendGPUs.length ||
|
|
280
|
+
systemInfo.gpu.gpuCount ||
|
|
281
|
+
1;
|
|
282
|
+
|
|
283
|
+
const totalVRAMFromUnified = typeof summary.totalVRAM === 'number' ? summary.totalVRAM : 0;
|
|
284
|
+
const totalVRAMFromFallback = dedicatedBackendGPUs.reduce((sum, gpu) => {
|
|
285
|
+
const amount = Number(gpu?.memory?.total || gpu?.memoryTotal || 0);
|
|
286
|
+
return sum + (Number.isFinite(amount) ? amount : 0);
|
|
287
|
+
}, 0);
|
|
288
|
+
const totalVRAM = totalVRAMFromUnified || totalVRAMFromFallback || systemInfo.gpu.vram;
|
|
289
|
+
const perGPUVRAM = dedicatedBackendGPUs[0]?.memory?.total ||
|
|
290
|
+
backendGPUs[0]?.memory?.total ||
|
|
291
|
+
(gpuCount > 0 && totalVRAM > 0 ? Math.round(totalVRAM / gpuCount) : 0);
|
|
292
|
+
|
|
293
|
+
const fallbackModel = dedicatedBackendGPUs[0]?.name || backendGPUs[0]?.name || null;
|
|
294
|
+
const modelFromUnified = summary.gpuInventory || summary.gpuModel || fallbackModel || systemInfo.gpu.model;
|
|
272
295
|
const vendor = this.inferVendorFromGPUModel(modelFromUnified, systemInfo.gpu.vendor);
|
|
273
296
|
|
|
274
297
|
systemInfo.gpu = {
|
|
@@ -277,11 +300,11 @@ class HardwareDetector {
|
|
|
277
300
|
vendor,
|
|
278
301
|
vram: totalVRAM || systemInfo.gpu.vram,
|
|
279
302
|
vramPerGPU: perGPUVRAM || systemInfo.gpu.vramPerGPU || 0,
|
|
280
|
-
dedicated: primaryType !== 'metal',
|
|
303
|
+
dedicated: hasFallbackDedicatedGpu ? true : primaryType !== 'metal',
|
|
281
304
|
gpuCount,
|
|
282
305
|
isMultiGPU: Boolean(summary.isMultiGPU || gpuCount > 1),
|
|
283
306
|
gpuInventory: summary.gpuInventory || null,
|
|
284
|
-
backend: primaryType,
|
|
307
|
+
backend: hasFallbackDedicatedGpu ? 'generic' : primaryType,
|
|
285
308
|
driverVersion: backendInfo.driver || systemInfo.gpu.driverVersion
|
|
286
309
|
};
|
|
287
310
|
} catch (error) {
|
|
@@ -10,6 +10,7 @@ const ROCmDetector = require('./backends/rocm-detector');
|
|
|
10
10
|
const IntelDetector = require('./backends/intel-detector');
|
|
11
11
|
const CPUDetector = require('./backends/cpu-detector');
|
|
12
12
|
const si = require('systeminformation');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
13
14
|
|
|
14
15
|
class UnifiedDetector {
|
|
15
16
|
constructor() {
|
|
@@ -363,6 +364,20 @@ class UnifiedDetector {
|
|
|
363
364
|
})
|
|
364
365
|
.filter(Boolean);
|
|
365
366
|
|
|
367
|
+
if (process.platform === 'linux') {
|
|
368
|
+
const lspciControllers = this.detectLinuxLspciGpus();
|
|
369
|
+
const knownKeys = new Set(
|
|
370
|
+
normalized.map((gpu) => this.getGpuMatchKey(gpu.name)).filter(Boolean)
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
for (const gpu of lspciControllers) {
|
|
374
|
+
const nameKey = this.getGpuMatchKey(gpu.name);
|
|
375
|
+
if (!nameKey || knownKeys.has(nameKey)) continue;
|
|
376
|
+
normalized.push(gpu);
|
|
377
|
+
knownKeys.add(nameKey);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
366
381
|
if (normalized.length === 0) {
|
|
367
382
|
return {
|
|
368
383
|
available: false,
|
|
@@ -381,7 +396,7 @@ class UnifiedDetector {
|
|
|
381
396
|
|
|
382
397
|
return {
|
|
383
398
|
available: true,
|
|
384
|
-
source: 'systeminformation',
|
|
399
|
+
source: process.platform === 'linux' ? 'systeminformation+lspci' : 'systeminformation',
|
|
385
400
|
gpus: normalized,
|
|
386
401
|
totalVRAM,
|
|
387
402
|
isMultiGPU: dedicated.length > 1,
|
|
@@ -389,6 +404,66 @@ class UnifiedDetector {
|
|
|
389
404
|
};
|
|
390
405
|
}
|
|
391
406
|
|
|
407
|
+
detectLinuxLspciGpus() {
|
|
408
|
+
try {
|
|
409
|
+
const lspciOutput = execSync('lspci -nn | grep -Ei "VGA|3D|Display"', {
|
|
410
|
+
encoding: 'utf8',
|
|
411
|
+
timeout: 8000,
|
|
412
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
413
|
+
});
|
|
414
|
+
return this.parseLinuxLspciGpus(lspciOutput);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
parseLinuxLspciGpus(lspciOutput = '') {
|
|
421
|
+
const lines = String(lspciOutput || '')
|
|
422
|
+
.split('\n')
|
|
423
|
+
.map((line) => line.trim())
|
|
424
|
+
.filter(Boolean);
|
|
425
|
+
|
|
426
|
+
const results = [];
|
|
427
|
+
const seen = new Set();
|
|
428
|
+
|
|
429
|
+
for (const line of lines) {
|
|
430
|
+
const lineLower = line.toLowerCase();
|
|
431
|
+
const isNvidia = lineLower.includes('nvidia');
|
|
432
|
+
const isAMD = lineLower.includes('amd') || lineLower.includes('ati') || lineLower.includes('radeon');
|
|
433
|
+
const isIntel = lineLower.includes('intel');
|
|
434
|
+
|
|
435
|
+
if (!isNvidia && !isAMD && !isIntel) continue;
|
|
436
|
+
|
|
437
|
+
const genericName = line
|
|
438
|
+
.replace(/^[0-9a-f:.]+\s+/i, '')
|
|
439
|
+
.replace(/\(rev\s+[0-9a-f]+\)$/i, '')
|
|
440
|
+
.trim();
|
|
441
|
+
|
|
442
|
+
const bracketName = line.match(/\[(?![0-9a-f]{4}:[0-9a-f]{4}\])([^\]]+)\]\s*\[[0-9a-f]{4}:[0-9a-f]{4}\]/i);
|
|
443
|
+
const name = (bracketName?.[1] || genericName || 'Unknown GPU').replace(/\s+/g, ' ').trim();
|
|
444
|
+
if (!name || name.toLowerCase() === 'unknown gpu') continue;
|
|
445
|
+
|
|
446
|
+
const isIntegrated = this.isIntegratedGPUModel(name) || isIntel;
|
|
447
|
+
let vram = this.estimateFallbackVRAM(name);
|
|
448
|
+
if (isIntegrated) {
|
|
449
|
+
vram = 0;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const dedupeKey = `${name.toLowerCase()}|${isIntegrated ? 'i' : 'd'}`;
|
|
453
|
+
if (seen.has(dedupeKey)) continue;
|
|
454
|
+
seen.add(dedupeKey);
|
|
455
|
+
|
|
456
|
+
results.push({
|
|
457
|
+
name,
|
|
458
|
+
vendor: isNvidia ? 'NVIDIA' : (isAMD ? 'AMD' : 'Intel'),
|
|
459
|
+
type: isIntegrated ? 'integrated' : 'dedicated',
|
|
460
|
+
memory: { total: vram }
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return results;
|
|
465
|
+
}
|
|
466
|
+
|
|
392
467
|
normalizeFallbackVRAM(value) {
|
|
393
468
|
const num = Number(value);
|
|
394
469
|
if (!Number.isFinite(num) || num <= 0) return 0;
|
|
@@ -426,6 +501,7 @@ class UnifiedDetector {
|
|
|
426
501
|
lower.includes('iris') ||
|
|
427
502
|
lower.includes('uhd') ||
|
|
428
503
|
lower.includes('hd graphics') ||
|
|
504
|
+
(lower.includes('radeon') && !lower.includes('radeon rx') && /\b\d{3,4}m\b/.test(lower)) ||
|
|
429
505
|
lower.includes('radeon graphics') ||
|
|
430
506
|
lower.includes('radeon(tm) graphics') ||
|
|
431
507
|
lower.includes('vega') ||
|
|
@@ -454,6 +530,23 @@ class UnifiedDetector {
|
|
|
454
530
|
return 0;
|
|
455
531
|
}
|
|
456
532
|
|
|
533
|
+
getGpuMatchKey(name) {
|
|
534
|
+
const lower = String(name || '').toLowerCase();
|
|
535
|
+
if (!lower) return '';
|
|
536
|
+
|
|
537
|
+
const familyMatch = lower.match(/\b(rtx|gtx|rx|arc)\s*([0-9]{3,4})\b/);
|
|
538
|
+
if (familyMatch) {
|
|
539
|
+
return `${familyMatch[1]}${familyMatch[2]}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const concise = lower
|
|
543
|
+
.replace(/nvidia|amd|ati|intel|corporation|geforce|radeon|graphics/g, '')
|
|
544
|
+
.replace(/\s+/g, ' ')
|
|
545
|
+
.trim();
|
|
546
|
+
|
|
547
|
+
return concise || lower;
|
|
548
|
+
}
|
|
549
|
+
|
|
457
550
|
/**
|
|
458
551
|
* Generate hardware fingerprint for benchmarks
|
|
459
552
|
*/
|
|
@@ -11,6 +11,7 @@ const crypto = require('crypto');
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const fetch = require('../utils/fetch');
|
|
14
|
+
const { evaluateFineTuningSupport } = require('./fine-tuning-support');
|
|
14
15
|
|
|
15
16
|
class AICheckSelector {
|
|
16
17
|
constructor() {
|
|
@@ -606,6 +607,7 @@ Return JSON with this structure:
|
|
|
606
607
|
chalk.bgMagenta.white.bold(' Det Score '),
|
|
607
608
|
chalk.bgMagenta.white.bold(' AI Score '),
|
|
608
609
|
chalk.bgMagenta.white.bold(' Final '),
|
|
610
|
+
chalk.bgMagenta.white.bold(' Fine-tune '),
|
|
609
611
|
chalk.bgMagenta.white.bold(' RAM '),
|
|
610
612
|
chalk.bgMagenta.white.bold(' Speed '),
|
|
611
613
|
chalk.bgMagenta.white.bold(' Status ')
|
|
@@ -621,6 +623,7 @@ Return JSON with this structure:
|
|
|
621
623
|
const finalScore = `${Math.round(candidate.finalScore)}/100`;
|
|
622
624
|
const ram = `${candidate.requiredGB}/${Math.round(results.hardware.usableMemGB)}GB`;
|
|
623
625
|
const speed = `${candidate.estTPS.toFixed(0)}t/s`;
|
|
626
|
+
const fineTuningSupport = evaluateFineTuningSupport(candidate, results.hardware || {});
|
|
624
627
|
|
|
625
628
|
let statusDisplay, modelDisplay;
|
|
626
629
|
if (isInstalled) {
|
|
@@ -637,6 +640,7 @@ Return JSON with this structure:
|
|
|
637
640
|
this.getScoreColor(candidate.score)(detScore),
|
|
638
641
|
candidate.aiScore ? this.getScoreColor(candidate.aiScore)(aiScore) : chalk.gray(aiScore),
|
|
639
642
|
this.getScoreColor(candidate.finalScore)(finalScore),
|
|
643
|
+
fineTuningSupport.shortLabel,
|
|
640
644
|
ram,
|
|
641
645
|
speed,
|
|
642
646
|
statusDisplay
|
|
@@ -648,11 +652,13 @@ Return JSON with this structure:
|
|
|
648
652
|
|
|
649
653
|
// Best recommendation section
|
|
650
654
|
const best = results.candidates[0];
|
|
655
|
+
const bestFineTuning = evaluateFineTuningSupport(best, results.hardware || {});
|
|
651
656
|
console.log('\n' + chalk.bgGreen.black.bold(' AI-POWERED RECOMMENDATION '));
|
|
652
657
|
console.log(chalk.green('╭' + '─'.repeat(50)));
|
|
653
658
|
console.log(chalk.green('│') + ` Best Model: ${chalk.cyan.bold(best.meta.name || best.meta.model_identifier)}`);
|
|
654
659
|
console.log(chalk.green('│') + ` Final Score: ${this.getScoreColor(best.finalScore)(Math.round(best.finalScore) + '/100')}`);
|
|
655
660
|
console.log(chalk.green('│') + ` ⚖️ Det: ${Math.round(best.score)} + AI: ${best.aiScore ? Math.round(best.aiScore) : 'N/A'}`);
|
|
661
|
+
console.log(chalk.green('│') + ` Fine-tuning: ${chalk.blue.bold(bestFineTuning.shortLabel)}`);
|
|
656
662
|
console.log(chalk.green('│'));
|
|
657
663
|
|
|
658
664
|
if (best.meta.installed) {
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fine-tuning suitability helper.
|
|
3
|
+
* Provides a simple hardware-aware estimate for Full FT / LoRA / QLoRA.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function toNumber(value) {
|
|
7
|
+
const num = Number(value);
|
|
8
|
+
return Number.isFinite(num) ? num : 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseModelParamsB(model = {}) {
|
|
12
|
+
const directParams = toNumber(model?.paramsB || model?.meta?.paramsB);
|
|
13
|
+
if (directParams > 0) return directParams;
|
|
14
|
+
|
|
15
|
+
const candidates = [
|
|
16
|
+
model?.size,
|
|
17
|
+
model?.model_identifier,
|
|
18
|
+
model?.identifier,
|
|
19
|
+
model?.name,
|
|
20
|
+
model?.meta?.model_identifier,
|
|
21
|
+
model?.meta?.name
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
const text = String(candidate || '');
|
|
26
|
+
if (!text) continue;
|
|
27
|
+
|
|
28
|
+
const billionMatch = text.match(/(\d+\.?\d*)\s*[bB]\b/);
|
|
29
|
+
if (billionMatch) {
|
|
30
|
+
return toNumber(billionMatch[1]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const millionMatch = text.match(/(\d+\.?\d*)\s*[mM]\b/);
|
|
34
|
+
if (millionMatch) {
|
|
35
|
+
return toNumber(millionMatch[1]) / 1000;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const gbMatch = text.match(/(\d+\.?\d*)\s*[gG][bB]\b/);
|
|
39
|
+
if (gbMatch) {
|
|
40
|
+
// Q4-style rule-of-thumb: ~0.5GB per 1B params.
|
|
41
|
+
return toNumber(gbMatch[1]) / 0.5;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const reqRam = toNumber(model?.requirements?.ram);
|
|
46
|
+
if (reqRam > 0) {
|
|
47
|
+
// Heuristic fallback: many model entries use ~1.2-1.5x RAM multiplier.
|
|
48
|
+
return Math.max(0, reqRam / 1.5);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getHardwareBudgetGB(hardware = {}) {
|
|
55
|
+
const totalRam = toNumber(
|
|
56
|
+
hardware?.memory?.total ||
|
|
57
|
+
hardware?.memory?.totalGB ||
|
|
58
|
+
hardware?.summary?.systemRAM ||
|
|
59
|
+
hardware?.systemRAM
|
|
60
|
+
);
|
|
61
|
+
const totalVram = toNumber(
|
|
62
|
+
hardware?.gpu?.vram ||
|
|
63
|
+
hardware?.gpu?.vramGB ||
|
|
64
|
+
hardware?.summary?.totalVRAM ||
|
|
65
|
+
hardware?.totalVRAM
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const gpuType = String(hardware?.gpu?.type || '').toLowerCase();
|
|
69
|
+
const gpuModel = String(hardware?.gpu?.model || '').toLowerCase();
|
|
70
|
+
const supportsMetal = Boolean(hardware?.acceleration?.supports_metal);
|
|
71
|
+
|
|
72
|
+
const isAppleUnified = supportsMetal ||
|
|
73
|
+
gpuType.includes('apple') ||
|
|
74
|
+
gpuModel.includes('apple') ||
|
|
75
|
+
/\bm[1-4]\b/.test(gpuModel);
|
|
76
|
+
|
|
77
|
+
const hasDedicatedLikeGpu = Boolean(
|
|
78
|
+
hardware?.gpu?.dedicated ||
|
|
79
|
+
totalVram > 0 ||
|
|
80
|
+
gpuType.includes('nvidia') ||
|
|
81
|
+
gpuType.includes('amd')
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (hasDedicatedLikeGpu && totalVram > 0) {
|
|
85
|
+
return {
|
|
86
|
+
budgetGB: totalVram,
|
|
87
|
+
accelerator: 'gpu'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isAppleUnified && totalRam > 0) {
|
|
92
|
+
// Keep headroom for OS and background apps.
|
|
93
|
+
return {
|
|
94
|
+
budgetGB: Math.max(0, Math.floor(totalRam * 0.7)),
|
|
95
|
+
accelerator: 'unified'
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
budgetGB: 0,
|
|
101
|
+
accelerator: 'none'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function estimateFineTuningMemoryGB(paramsB) {
|
|
106
|
+
// Coarse practical estimates for local fine-tuning workflows.
|
|
107
|
+
return {
|
|
108
|
+
fullFineTuning: Math.ceil(paramsB * 8 + 4),
|
|
109
|
+
lora: Math.ceil(paramsB * 1.8 + 3),
|
|
110
|
+
qlora: Math.ceil(paramsB * 0.8 + 2)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function evaluateFineTuningSupport(model = {}, hardware = {}) {
|
|
115
|
+
const paramsB = parseModelParamsB(model);
|
|
116
|
+
if (paramsB <= 0) {
|
|
117
|
+
return {
|
|
118
|
+
method: 'unknown',
|
|
119
|
+
label: 'Unknown',
|
|
120
|
+
shortLabel: 'Unknown',
|
|
121
|
+
supports: {
|
|
122
|
+
fullFineTuning: false,
|
|
123
|
+
lora: false,
|
|
124
|
+
qlora: false
|
|
125
|
+
},
|
|
126
|
+
requirementsGB: {
|
|
127
|
+
fullFineTuning: 0,
|
|
128
|
+
lora: 0,
|
|
129
|
+
qlora: 0
|
|
130
|
+
},
|
|
131
|
+
paramsB: 0,
|
|
132
|
+
budgetGB: 0,
|
|
133
|
+
accelerator: 'none'
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { budgetGB, accelerator } = getHardwareBudgetGB(hardware);
|
|
138
|
+
const requirementsGB = estimateFineTuningMemoryGB(paramsB);
|
|
139
|
+
|
|
140
|
+
const canFull = budgetGB >= requirementsGB.fullFineTuning;
|
|
141
|
+
const canLoRA = budgetGB >= requirementsGB.lora;
|
|
142
|
+
const canQLoRA = budgetGB >= requirementsGB.qlora;
|
|
143
|
+
|
|
144
|
+
if (canFull) {
|
|
145
|
+
return {
|
|
146
|
+
method: 'full_ft',
|
|
147
|
+
label: 'Full FT / LoRA / QLoRA',
|
|
148
|
+
shortLabel: 'Full+LoRA+QLoRA',
|
|
149
|
+
supports: {
|
|
150
|
+
fullFineTuning: true,
|
|
151
|
+
lora: true,
|
|
152
|
+
qlora: true
|
|
153
|
+
},
|
|
154
|
+
requirementsGB,
|
|
155
|
+
paramsB,
|
|
156
|
+
budgetGB,
|
|
157
|
+
accelerator
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (canLoRA) {
|
|
162
|
+
return {
|
|
163
|
+
method: 'lora',
|
|
164
|
+
label: 'LoRA / QLoRA',
|
|
165
|
+
shortLabel: 'LoRA+QLoRA',
|
|
166
|
+
supports: {
|
|
167
|
+
fullFineTuning: false,
|
|
168
|
+
lora: true,
|
|
169
|
+
qlora: true
|
|
170
|
+
},
|
|
171
|
+
requirementsGB,
|
|
172
|
+
paramsB,
|
|
173
|
+
budgetGB,
|
|
174
|
+
accelerator
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (canQLoRA) {
|
|
179
|
+
return {
|
|
180
|
+
method: 'qlora',
|
|
181
|
+
label: 'QLoRA only',
|
|
182
|
+
shortLabel: 'QLoRA',
|
|
183
|
+
supports: {
|
|
184
|
+
fullFineTuning: false,
|
|
185
|
+
lora: false,
|
|
186
|
+
qlora: true
|
|
187
|
+
},
|
|
188
|
+
requirementsGB,
|
|
189
|
+
paramsB,
|
|
190
|
+
budgetGB,
|
|
191
|
+
accelerator
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
method: 'none',
|
|
197
|
+
label: accelerator === 'none' ? 'No accelerator' : 'Not suitable',
|
|
198
|
+
shortLabel: accelerator === 'none' ? 'No accel' : 'No FT',
|
|
199
|
+
supports: {
|
|
200
|
+
fullFineTuning: false,
|
|
201
|
+
lora: false,
|
|
202
|
+
qlora: false
|
|
203
|
+
},
|
|
204
|
+
requirementsGB,
|
|
205
|
+
paramsB,
|
|
206
|
+
budgetGB,
|
|
207
|
+
accelerator
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
parseModelParamsB,
|
|
213
|
+
estimateFineTuningMemoryGB,
|
|
214
|
+
evaluateFineTuningSupport
|
|
215
|
+
};
|