// PEFT Anti-Pattern Checker (v0.8.3 anti-bullshit pack #9) // // Static linter for PEFT / LoRA training code paths. Targets the most // expensive bugs the community has documented: silent base-model loads // (peft #2115), QLoRA ordering errors, and arch/target_modules mismatch. // // Pain (Solutions Hub `peft_loading`): `get_peft_model()` called before // `PeftModel.from_pretrained()` silently loads the base model and a // FRESH adapter, ignoring the user's saved LoRA weights. Hours of // training thrown away with no error. // // Source citations: // - peft #2115 — original silent-load bug // - https://huggingface.co/docs/peft/main/en/developer_guides/troubleshooting // - PEFT `get_layer_status() / get_model_status()` runtime check // // Pure logic — no human strings. Returns codes+params; main.js does // the i18n lookup. Same shape as json_cot_linter.js. // ============================================================================= // Token/pattern definitions // ============================================================================= // Rough comment + string stripping. NOT a real Python parser; we only // need to remove obvious noise so regex matches don't fire inside // docstrings or commented-out code. Anything still in scope after this // is treated as "live" Python. function stripCommentsAndStrings(code) { // Remove triple-quoted strings (greedy match across newlines) let s = code.replace(/"""[\s\S]*?"""/g, ""); s = s.replace(/'''[\s\S]*?'''/g, ""); // Remove single-line strings (but keep the line so line numbers stay valid) s = s.replace(/"(?:\\.|[^"\\\n])*"/g, '""'); s = s.replace(/'(?:\\.|[^'\\\n])*'/g, "''"); // Remove `# ...` comments to end of line s = s.replace(/#[^\n]*/g, ""); return s; } function findFirstMatchLine(stripped, pattern) { const lines = stripped.split("\n"); for (let i = 0; i < lines.length; i++) { if (pattern.test(lines[i])) return i + 1; // 1-indexed } return null; } function findAllMatchLines(stripped, pattern) { const out = []; const lines = stripped.split("\n"); for (let i = 0; i < lines.length; i++) { if (pattern.test(lines[i])) out.push(i + 1); } return out; } // Extract STRING LITERALS from the ORIGINAL code (we kept them in the // raw text so we can scan their contents for adapter/checkpoint hints). // Returns array of { value, line }. function extractStringLiterals(code) { const out = []; const re = /(["'])((?:\\.|(?!\1)[^\\\n])*)\1/g; const lines = code.split("\n"); let lineStart = 0; for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { const line = lines[lineIdx]; let m; re.lastIndex = 0; const lineRe = new RegExp(re.source, re.flags); while ((m = lineRe.exec(line)) !== null) { out.push({ value: m[2], line: lineIdx + 1 }); } } return out; } // Extract `target_modules=[...]` literal lists. Returns array of // { modules: [..], line }. Best-effort; only catches literal lists. function extractTargetModules(code) { const out = []; const re = /target_modules\s*=\s*\[([^\]]*)\]/g; let m; while ((m = re.exec(code)) !== null) { const inner = m[1]; const modules = (inner.match(/["']([^"']+)["']/g) || []).map(s => s.slice(1, -1)); // Compute line number const before = code.slice(0, m.index); const line = before.split("\n").length; out.push({ modules, line }); } return out; } // Extract `r=N` and `lora_alpha=N` from the same call site. Best-effort. function extractLoraConfig(code) { const out = []; // Find LoraConfig(...) calls and capture the args block (single-line or balanced). const re = /LoraConfig\s*\(([^)]*)\)/g; let m; while ((m = re.exec(code)) !== null) { const args = m[1]; const r = args.match(/\br\s*=\s*(\d+)/); const alpha = args.match(/lora_alpha\s*=\s*(\d+)/); const before = code.slice(0, m.index); const line = before.split("\n").length; out.push({ r: r ? parseInt(r[1], 10) : null, lora_alpha: alpha ? parseInt(alpha[1], 10) : null, line, }); } return out; } // ============================================================================= // Architecture → conventional target_modules // ============================================================================= // Mapping built from public PEFT docs + transformers configs. Conservative: // only architectures with stable, well-documented module names. When the // user's target_modules don't match the listed arch family, we flag it. const ARCH_TARGET_MODULES = { llama: ["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"], mistral: ["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"], qwen2: ["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"], phi: ["q_proj","k_proj","v_proj","dense","fc1","fc2"], phi3: ["qkv_proj","o_proj","gate_up_proj","down_proj"], gemma: ["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"], falcon: ["query_key_value","dense","dense_h_to_4h","dense_4h_to_h"], bloom: ["query_key_value","dense","dense_h_to_4h","dense_4h_to_h"], gpt2: ["c_attn","c_proj","c_fc"], gptneox: ["query_key_value","dense","dense_h_to_4h","dense_4h_to_h"], mpt: ["Wqkv","out_proj","up_proj","down_proj"], }; // Token hints in HF model ids that map to the keys above. Any one of // these matching is enough to claim "the user is targeting arch X". const ARCH_ID_HINTS = { llama: /\b(?:llama|llama-?[123]|tinyllama|vicuna|alpaca|deepseek|mixtral)\b/i, mistral: /\bmistral\b/i, qwen2: /\bqwen2?\b/i, phi: /\bphi-?[12]\b/i, phi3: /\bphi-?3\b/i, gemma: /\bgemma\b/i, falcon: /\bfalcon\b/i, bloom: /\bbloom\b/i, gpt2: /\bgpt-?2\b/i, gptneox: /\b(?:gpt-?neox|pythia|dolly)\b/i, mpt: /\bmpt\b/i, }; function detectArch(stringLiterals) { for (const lit of stringLiterals) { for (const [arch, hint] of Object.entries(ARCH_ID_HINTS)) { if (hint.test(lit.value)) { return { arch, source: lit.value, line: lit.line }; } } } return null; } // ============================================================================= // Heuristics for detecting "this string is a saved adapter checkpoint path" // ============================================================================= const CHECKPOINT_HINT_RE = /(?:adapter[_-]?(?:config|model)|adapter\.safetensors|adapter_model\.bin|peft[_-]?model|lora[_-]?weights?|checkpoint(?:[-_/]\d+)?|\boutput[_-]?dir\b|trained?[_-]?lora)/i; function findAdapterCheckpointHint(stringLiterals) { for (const lit of stringLiterals) { if (CHECKPOINT_HINT_RE.test(lit.value)) return lit; } return null; } // ============================================================================= // Public entry point // ============================================================================= const RULES = { // Strong correctness issues — almost certainly a bug silent_base_load: { severity: "error" }, qlora_order: { severity: "error" }, target_modules_mismatch: { severity: "warning" }, // Optional / informational alpha_not_2r: { severity: "info" }, no_peft_calls: { severity: "info" }, }; export function lintPeftCode(text) { if (typeof text !== "string" || !text.trim()) { return { code: "empty_input", findings: [] }; } const stripped = stripCommentsAndStrings(text); // Bail if no PEFT-related calls at all — unhelpful otherwise. const hasGetPeftModel = /\bget_peft_model\s*\(/.test(stripped); const hasFromPretrained = /\bPeftModel\s*\.\s*from_pretrained\s*\(/.test(stripped); const hasPrepareKbit = /\bprepare_model_for_kbit_training\s*\(/.test(stripped); const hasLoraConfig = /\bLoraConfig\s*\(/.test(stripped); const hasBnbConfig = /\bBitsAndBytesConfig\s*\(/.test(stripped); if ( !hasGetPeftModel && !hasFromPretrained && !hasPrepareKbit && !hasLoraConfig ) { return { code: "no_peft_calls", findings: [{ rule: "no_peft_calls", severity: "info", line: null, params: {}, }], }; } const findings = []; const stringLiterals = extractStringLiterals(text); // ─── Rule A: silent base-model load (peft #2115) ───────────────────────── // Pattern: `get_peft_model(...)` is the only model-creation path AND // there's a string literal that looks like a saved adapter path. // Likely user wants to LOAD a saved adapter but is creating a new one. if (hasGetPeftModel && !hasFromPretrained) { const hint = findAdapterCheckpointHint(stringLiterals); if (hint) { const getPeftLine = findFirstMatchLine(stripped, /\bget_peft_model\s*\(/); findings.push({ rule: "silent_base_load", severity: "error", line: getPeftLine, params: { checkpoint_hint: hint.value, checkpoint_line: hint.line, fix: `PeftModel.from_pretrained(base_model, ${JSON.stringify(hint.value)})`, }, }); } } // ─── Rule B: QLoRA ordering — prepare_model_for_kbit_training AFTER get_peft_model ── if (hasPrepareKbit && hasGetPeftModel) { const prepLine = findFirstMatchLine(stripped, /\bprepare_model_for_kbit_training\s*\(/); const peftLine = findFirstMatchLine(stripped, /\bget_peft_model\s*\(/); if (prepLine !== null && peftLine !== null && prepLine > peftLine) { findings.push({ rule: "qlora_order", severity: "error", line: prepLine, params: { prepare_line: prepLine, get_peft_model_line: peftLine, }, }); } } // ─── Rule C: target_modules / arch mismatch ───────────────────────────── const targetModuleCalls = extractTargetModules(text); const detectedArch = detectArch(stringLiterals); if (targetModuleCalls.length > 0 && detectedArch !== null) { const expected = ARCH_TARGET_MODULES[detectedArch.arch]; if (expected) { const expectedSet = new Set(expected); for (const tm of targetModuleCalls) { if (tm.modules.length === 0) continue; const hits = tm.modules.filter(m => expectedSet.has(m)).length; const ratio = hits / tm.modules.length; // Less than half of user's specified modules are in the expected list. if (ratio < 0.5) { findings.push({ rule: "target_modules_mismatch", severity: "warning", line: tm.line, params: { user_modules: tm.modules, detected_arch: detectedArch.arch, detected_from: detectedArch.source, expected_modules: expected, hits, total: tm.modules.length, }, }); } } } } // ─── Rule D: lora_alpha ≠ 2*r convention ──────────────────────────────── // Common rule of thumb: alpha = 2*r gives roughly unit-scale LoRA. // alpha = r is also seen but reduces effective LR. Anything else is // worth surfacing as info. const loraCfgs = extractLoraConfig(text); for (const cfg of loraCfgs) { if (cfg.r != null && cfg.lora_alpha != null) { const ratio = cfg.lora_alpha / cfg.r; if (ratio !== 1 && ratio !== 2) { findings.push({ rule: "alpha_not_2r", severity: "info", line: cfg.line, params: { r: cfg.r, lora_alpha: cfg.lora_alpha, ratio: Math.round(ratio * 100) / 100, }, }); } } } // ─── Aggregate verdict code ────────────────────────────────────────────── let code; if (findings.length === 0) { code = "clean"; } else if (findings.some(f => f.severity === "error")) { code = "errors_found"; } else if (findings.some(f => f.severity === "warning")) { code = "warnings_only"; } else { code = "info_only"; } return { code, findings, summary: { total: findings.length } }; } export { ARCH_TARGET_MODULES, ARCH_ID_HINTS };