Skip to content

feat: add nanotune clean and report fused/ cache disk usage - #112

Open
rohanshrma222 wants to merge 4 commits into
Nano-Collective:mainfrom
rohanshrma222:multigb
Open

feat: add nanotune clean and report fused/ cache disk usage#112
rohanshrma222 wants to merge 4 commits into
Nano-Collective:mainfrom
rohanshrma222:multigb

Conversation

@rohanshrma222

@rohanshrma222 rohanshrma222 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #70 ; nanotune export fuses the LoRA adapter into a full-precision copy at .nanotune/models/fused/ (multi-GB) and never reports or cleans it up. This PR keeps the cache (it speeds up repeat exports via --skip-fuse) but makes its cost visible and reclaimable:

  • nanotune export and nanotune status now report the fused cache's path and size.
  • New nanotune clean command removes it (interactive confirm, or -y/--yes for CI/scripts).
  • --skip-fuse now fails fast with a clear message if no fused/ cache exists, instead of a cryptic error deep in GGUF conversion.
  • Docs updated: export.md, status.md, index.md, and new clean.md.

Edit:
nanotune clean is deliberately scoped to the fused/ cache only for this PR — it doesn't also cover the larger base-model cache at ~/.nanotune/models/base-cache. Follow-up tracked in #125: sharded-model detection in hasUsableFusedModel, the unguarded top-level readdirSync, and extending clean to cover the base-model cache.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Testing

Automated Tests

  • All existing tests pass (pnpm test:all completes successfully)
  • New tests added for new functionality (if applicable)

Note: I verified with tsc --noEmit, knip, biome check on changed files, and targeted ava runs (export.spec.ts + commands.spec.tsx, 21/21 passing) rather than the full pnpm test:all. This dev machine is Windows, and the full suite has pre-existing, unrelated Windows-only failures (AVA EPERM races on shared temp dirs, POSIX file-mode assertions) — confirmed reproducible on an unmodified main. nanotune itself is Apple-Silicon-only (assertSupportedPlatform()), so full manual/e2e testing needs a macOS run — see below.

Manual Testing

  • Tested nanotune init
  • Tested nanotune data commands (add/import/list/validate)
  • Tested nanotune train
  • Tested nanotune export
  • Tested nanotune benchmark

Not run manually — this dev environment is Windows/x64, and nanotune hard-requires macOS on Apple Silicon (assertSupportedPlatform() guards export/train). Instead, the new --skip-fuse validation was factored into a pure function (skipFuseValidationError) and unit-tested directly so it doesn't depend on platform, and clean/status were exercised via ink-testing-library component tests covering: no project, nothing to clean, --yes deletion, and interactive confirm-without-deleting. Would appreciate a maintainer's manual pass on real hardware before merge, particularly exportstatusexport --skip-fusecleanexport --skip-fuse (should now fail clearly).

Checklist

  • Code follows project style guidelines (pnpm format)
  • Self-review completed
  • Documentation updated (if needed)
  • No breaking changes (or clearly documented)

@will-lamerton will-lamerton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, it's a well-scoped fix for #70 and it follows the existing patterns closely (useKeyInput/useAutoExit/ExitHint, the (y/n) prompt convention, the data import style non-TTY gate, lazy command import). Capturing the size label before rmSync so "Freed:" stays accurate was a nice touch. A few things to sort before merge.

Blocking

1. The --skip-fuse guard is weaker than the problem it fixes (src/commands/export.tsx)

It only checks that the directory exists. mlx_lm.fuse creates --save-path before it finishes writing, so an interrupted or failed export leaves a present-but-incomplete fused/, and the user gets exactly the cryptic mid-GGUF-conversion failure this PR set out to replace. Please check for a real artifact inside (config.json, or any *.safetensors) rather than the bare directory.

The same check drives clean's branching, so a leftover empty fused/ currently prompts to free 0 B instead of saying "Nothing to clean". A shared hasUsableFusedModel() helper fixes both in one place.

2. getDirectorySize can throw and take nanotune status down with it (src/lib/config.ts)

It uses statSync, which follows symlinks, so a broken symlink or an unreadable entry throws. StatusCommand calls it in the render body, which turns that into a crash in a command that never touched this directory before. Either use lstatSync and skip non-regular entries, or wrap the per-entry stat in try/catch and skip failures.

3. Missing tests

  • src/lib/config.spec.ts covers every other function in that module, but the new formatFileSize and getDirectorySize have no direct tests. Worth pinning the B/KB/MB/GB thresholds, nested directories, and the missing-directory-returns-0 path.
  • The interactive delete path is untested: the --yes test starts in cleaning, and the confirm test stops at the prompt. Nothing covers keypress y leading to rmSync, or n/Escape exiting without deleting. commands.spec.tsx already has a useKeyInput harness for this.

Non-blocking

  • cli.tsx does await import('node:fs') although the file already statically imports from node:fs at the top. Add existsSync to the static import.
  • In clean.tsx, errorMessage is initialised to the no-project string and then disambiguated by hasProject at render time, so one state variable carries two meanings. A string | null error state, or an early return for the no-project case, reads more clearly.
  • Design question worth settling before this interface is public: nanotune clean is a broad name for a command that removes one specific cache. There's also the base model cache (lib/model-cache.ts), usually larger than fused/, that users will plausibly expect clean to handle. Either scope the name or give the command a target flag.
  • getDirectorySize runs on every StatusCommand render rather than memoized. Fine in practice for a handful of model files, but useMemo costs nothing.

No security concerns: the rmSync target is derived from cwd plus fixed path constants with no user input, deletion is confirmed by default, and --yes is opt-in.

On testing

Completely understood on the Windows/Apple Silicon constraint, and factoring skipFuseValidationError into a pure function to work around it was the right instinct. I'll do a macOS run of pnpm test:all plus the manual sequence you suggested (export -> status -> export --skip-fuse -> clean -> export --skip-fuse) once the above is addressed. I'll add an interrupted-export case to that pass, since that's where the --skip-fuse guard is weakest today.

@rohanshrma222

Copy link
Copy Markdown
Contributor Author

On the clean scope question: keeping it fused/-only for this PR; matches the original issue #70 boundary. Filing a follow-up for a --target/--all flag (or a nanotune cache subcommand) to also cover the base-model cache, alongside the clean/export race that's already tracked. Happy to take either as a next PR if you'd rather see it split differently.

@will-lamerton will-lamerton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rohanshrma222, this covers the blocking points well. The shared hasUsableFusedModel() helper is the right shape, and the interactive y/n/Escape coverage plus the interrupted-export case are exactly what was missing. I ran the suite on macOS: 88 passing, all checks green.

One thing slipped through. src/commands/status.tsx:47 still uses existsSync(fusedDir) rather than the new helper, so an interrupted export contradicts itself:

existsSync(fusedDir)  : true
hasUsableFusedModel() : false

status prints Fused model cache: 3 B (run nanotune clean to remove), and then clean says Nothing to clean. Swapping that line to hasUsableFusedModel(fusedDir) fixes it. Worth a test alongside it, since the current status test writes a complete fused model.

Two smaller ones that belong in the follow-up issue rather than this PR:

  • hasUsableFusedModel passes on any single .safetensors, so a sharded model interrupted after shard 1 of 3 still reads as usable.
  • The top-level readdirSync in getDirectorySize and in hasUsableFusedModel are unguarded, so EACCES on the directory itself can still take status down. Your change closed the statSync case I flagged, just not that one.

On the clean naming: agreed, keep it fused/-only here. Please open the follow-up for the base model cache and link it in the description.

Once status.tsx is updated I'll do the macOS manual pass and approve.

@rohanshrma222

Copy link
Copy Markdown
Contributor Author

Thanks @rohanshrma222, this covers the blocking points well. The shared hasUsableFusedModel() helper is the right shape, and the interactive y/n/Escape coverage plus the interrupted-export case are exactly what was missing. I ran the suite on macOS: 88 passing, all checks green.

One thing slipped through. src/commands/status.tsx:47 still uses existsSync(fusedDir) rather than the new helper, so an interrupted export contradicts itself:

existsSync(fusedDir)  : true
hasUsableFusedModel() : false

status prints Fused model cache: 3 B (run nanotune clean to remove), and then clean says Nothing to clean. Swapping that line to hasUsableFusedModel(fusedDir) fixes it. Worth a test alongside it, since the current status test writes a complete fused model.

Two smaller ones that belong in the follow-up issue rather than this PR:

  • hasUsableFusedModel passes on any single .safetensors, so a sharded model interrupted after shard 1 of 3 still reads as usable.
  • The top-level readdirSync in getDirectorySize and in hasUsableFusedModel are unguarded, so EACCES on the directory itself can still take status down. Your change closed the statSync case I flagged, just not that one.

On the clean naming: agreed, keep it fused/-only here. Please open the follow-up for the base model cache and link it in the description.

Once status.tsx is updated I'll do the macOS manual pass and approve.

Thx for the review. Yes, status.tsx is still using the old check which will make the implementation confusing. I will make all the suggested changes.

@rohanshrma222

rohanshrma222 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

I have raised the new follow-up issue and have mentioned the issue here in the description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-GB fused/ directory is left behind after export

2 participants