File size: 7,107 Bytes
06497ff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | #!/usr/bin/env node
// forge β tune and convert models for AMD Ryzen AI NPUs, one command.
//
// forge tune <chats.jsonl> [--name grandma] [--base unsloth/Llama-3.2-1B-Instruct]
// [--donor-tag llama3.2:1b] [--epochs 3]
// forge convert <hf-repo> <file.gguf> [--name mymodel] [--donor-tag llama3.2:1b]
// forge register merge registry.json into FLM's list (UAC if needed)
// forge list stock models + forge customs with status
// forge doctor NPU stack / registry / port health checks
// forge serve <tag> flm serve passthrough (performance pmode)
'use strict';
const fs = require('fs');
const path = require('path');
const { spawnSync, spawn } = require('child_process');
const FORGE = __dirname;
const FLM_LIST = 'C:\\Program Files\\flm\\model_list.json';
const FLM_MODELS = path.join(process.env.USERPROFILE, 'Documents', 'flm', 'models');
const ENV = { ...process.env, PYTHONIOENCODING: 'utf-8' };
const argv = process.argv.slice(2);
const cmd = argv[0];
function flag(name, def) { const i = argv.indexOf('--' + name); return i > 0 ? argv[i + 1] : def; }
function run(exe, args, opts = {}) {
const r = spawnSync(exe, args, { stdio: 'inherit', env: ENV, shell: true, ...opts });
return r.status === 0;
}
function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8').replace(/^ο»Ώ/, '')); } catch (_) { return null; } }
function assemble(outName, tag, donorTag, donor) {
return run('node', [path.join(FORGE, 'bin', 'assemble.js'), '--out', outName,
'--donor', donor, '--tag', tag, '--donor-tag', donorTag]);
}
function donorFolderFor(donorTag) {
const ml = readJson(FLM_LIST) || { models: {} };
const [k, s] = (donorTag || 'llama3.2:1b').split(':');
const e = ml.models[k] && ml.models[k][s];
if (!e) { console.error('donor tag not found in FLM list: ' + donorTag); process.exit(1); }
return e.name;
}
if (cmd === 'tune') {
const data = argv[1];
if (!data || !fs.existsSync(data)) { console.error('forge tune <chats.jsonl> β file not found'); process.exit(1); }
const name = flag('name', path.basename(data, path.extname(data)));
const base = flag('base', 'unsloth/Llama-3.2-1B-Instruct');
const donorTag = flag('donor-tag', 'llama3.2:1b');
const epochs = flag('epochs', '3');
const outName = 'forge-' + name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
// stage data where the modal mount expects it
const stage = path.join(FORGE, 'scratch', 'tune-data');
fs.mkdirSync(stage, { recursive: true });
fs.copyFileSync(data, path.join(stage, 'data.jsonl'));
console.log(`forge tune: ${data} -> ${base} (${epochs} epochs) -> NPU as ${name}-forge`);
if (!run('modal', ['run', path.join(FORGE, 'modal', 'tune_npu.py'),
'--base', base, '--data-file', 'data.jsonl', '--out-name', outName, '--epochs', epochs])) {
console.error('tune job failed β see output above'); process.exit(1);
}
if (!assemble(outName, name.toLowerCase() + '-forge:1b', donorTag, donorFolderFor(donorTag))) process.exit(1);
console.log('\nDone. Run: forge register then: flm run ' + name.toLowerCase() + '-forge:1b');
}
else if (cmd === 'convert') {
const repo = argv[1], file = argv[2];
if (!repo || !file) { console.error('forge convert <hf-repo> <file.gguf>'); process.exit(1); }
const name = flag('name', file.replace(/\.gguf$/i, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30));
const donorTag = flag('donor-tag', 'llama3.2:1b');
const outName = 'forge-' + name;
console.log(`forge convert: ${repo}/${file} -> NPU as ${name}-forge`);
if (!run('modal', ['run', path.join(FORGE, 'modal', 'convert_q4nx.py'),
'--repo-id', repo, '--filename', file, '--out-name', outName])) {
console.error('convert job failed'); process.exit(1);
}
const size = donorTag.split(':')[1] || '1b';
if (!assemble(outName, name + '-forge:' + size, donorTag, donorFolderFor(donorTag))) process.exit(1);
console.log('\nDone. Run: forge register then: flm run ' + name + '-forge:' + size);
}
else if (cmd === 'register') {
const r = spawnSync('node', [path.join(FORGE, 'bin', 'register.js')], { encoding: 'utf8', env: ENV, shell: true });
process.stdout.write(r.stdout || '');
if (r.status !== 0) {
console.log('(needs admin β launching UAC prompt via register-admin.bat)');
spawn('cmd', ['/c', 'start', '', path.join(FORGE, 'register-admin.bat')], { detached: true, shell: false });
}
}
else if (cmd === 'list') {
run('flm', ['list', '--filter', 'installed']);
const reg = readJson(path.join(FORGE, 'registry.json')) || { models: {} };
const ml = readJson(FLM_LIST) || { models: {} };
console.log('\nforge customs:');
for (const [k, sizes] of Object.entries(reg.models)) {
for (const [s, e] of Object.entries(sizes)) {
const registered = !!(ml.models[k] && ml.models[k][s]);
const folder = fs.existsSync(path.join(FLM_MODELS, e.name));
console.log(` ${k}:${s} files:${folder ? 'OK' : 'MISSING'} registered:${registered ? 'YES' : 'no β run forge register'}`);
}
}
}
else if (cmd === 'doctor') {
console.log('β NPU device β');
run('powershell', ['-NoProfile', '-Command', '"Get-PnpDevice -Class ComputeAccelerator | Select-Object Status, FriendlyName | Format-Table -HideTableHeaders"']);
console.log('β NPU driver version (FLM needs >= 32.0.203.304, recommends .311) β');
const dv = spawnSync('powershell', ['-NoProfile', '-Command', '"Get-PnpDevice -Class ComputeAccelerator | ForEach-Object { (Get-PnpDeviceProperty -InstanceId $_.InstanceId -KeyName DEVPKEY_Device_DriverVersion).Data }"'], { encoding: 'utf8', shell: true });
const ver = (dv.stdout || '').trim().split('\n')[0] || 'unknown';
const last = parseInt((ver.match(/(\d+)$/) || [])[1] || '0', 10);
console.log(` installed: ${ver}` + (last && last < 304 ? ' -> TOO OLD: update the AMD NPU driver (AMD Software / amd.com Ryzen AI driver), then re-run doctor' : last ? ' -> OK' : ''));
console.log('β flm validate β');
run('flm', ['validate']);
console.log('β registry drift (customs lost to an flm update?) β');
const reg = readJson(path.join(FORGE, 'registry.json')) || { models: {} };
const ml = readJson(FLM_LIST) || { models: {} };
let drift = 0;
for (const [k, sizes] of Object.entries(reg.models)) for (const s of Object.keys(sizes)) {
if (!(ml.models[k] && ml.models[k][s])) { console.log(` LOST: ${k}:${s} β run forge register`); drift++; }
}
if (!drift) console.log(' all forge customs registered');
console.log('β port 52625 β');
const ns = spawnSync('netstat', ['-ano'], { encoding: 'utf8' });
console.log((ns.stdout || '').split('\n').some(l => l.includes(':52625') && l.includes('LISTENING')) ? ' flm serve is UP' : ' free (flm not serving)');
}
else if (cmd === 'serve') {
const tag = argv[1];
if (!tag) { console.error('forge serve <tag>'); process.exit(1); }
run('flm', ['serve', tag, '--pmode', 'performance']);
}
else {
console.log(fs.readFileSync(__filename, 'utf8').split('\n').slice(1, 11).map(l => l.replace(/^\/\/ ?/, '')).join('\n'));
}
|