feat: VRAM-based model filtering in frontend
This commit is contained in:
parent
6ed179b459
commit
f285b5379a
7 changed files with 415 additions and 15 deletions
|
|
@ -9,6 +9,8 @@
|
|||
"@assistant-ui/react-markdown": "^0.12.1",
|
||||
"@assistant-ui/react-streamdown": "^0.1.0",
|
||||
"@base-ui/react": "^1.1.0",
|
||||
"@dagrejs/dagre": "^2.0.4",
|
||||
"@dagrejs/graphlib": "^3.0.4",
|
||||
"@fontsource-variable/figtree": "^5.2.10",
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@fontsource-variable/space-grotesk": "^5.2.10",
|
||||
|
|
@ -75,6 +77,7 @@
|
|||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4",
|
||||
"vitest": "^4.0.18",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -189,6 +192,10 @@
|
|||
|
||||
"@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="],
|
||||
|
||||
"@dagrejs/dagre": ["@dagrejs/dagre@2.0.4", "", { "dependencies": { "@dagrejs/graphlib": "3.0.4" } }, "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA=="],
|
||||
|
||||
"@dagrejs/graphlib": ["@dagrejs/graphlib@3.0.4", "", {}, "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg=="],
|
||||
|
||||
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
|
||||
|
||||
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.52.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w=="],
|
||||
|
|
@ -679,6 +686,8 @@
|
|||
|
||||
"@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
|
@ -743,6 +752,8 @@
|
|||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
|
@ -797,6 +808,20 @@
|
|||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||
|
||||
"@xyflow/react": ["@xyflow/react@12.10.0", "", { "dependencies": { "@xyflow/system": "0.0.74", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw=="],
|
||||
|
|
@ -825,6 +850,8 @@
|
|||
|
||||
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"assistant-cloud": ["assistant-cloud@0.1.15", "", { "dependencies": { "assistant-stream": "^0.3.0" } }, "sha512-LK+HrE6p1/jefzH3IosGUfpZvsM8wLPCsd955TLPEfJ9rMknl9KeE2QxRMHl3Ob17m33NqmJi/t/oWWKdgP6Nw=="],
|
||||
|
||||
"assistant-stream": ["assistant-stream@0.3.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-gp5wXZiH7fiPdCByusRZ6ZQrmifAjiuDTenR0HSeviiRLXmirePxePvTgHPT1ZahohCLroge7fjtC9vAc+Hqsg=="],
|
||||
|
|
@ -865,6 +892,8 @@
|
|||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
|
@ -1089,6 +1118,8 @@
|
|||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||
|
|
@ -1121,6 +1152,8 @@
|
|||
|
||||
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
|
@ -1133,6 +1166,8 @@
|
|||
|
||||
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
|
||||
|
||||
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
||||
|
|
@ -1591,6 +1626,8 @@
|
|||
|
||||
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
|
@ -1821,6 +1858,8 @@
|
|||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
|
@ -1835,8 +1874,12 @@
|
|||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||
|
||||
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
|
||||
|
||||
"streamdown": ["streamdown@2.1.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.1.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-u9gWd0AmjKg1d+74P44XaPlGrMeC21oDOSIhjGNEYMAttDMzCzlJO6lpTyJ9JkSinQQF65YcK4eOd3q9iTvULw=="],
|
||||
|
|
@ -1883,10 +1926,14 @@
|
|||
|
||||
"tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||
|
||||
"tldts": ["tldts@7.0.19", "", { "dependencies": { "tldts-core": "^7.0.19" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA=="],
|
||||
|
||||
"tldts-core": ["tldts-core@7.0.19", "", {}, "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A=="],
|
||||
|
|
@ -1993,6 +2040,8 @@
|
|||
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
|
||||
"vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
|
||||
|
||||
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||
|
||||
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
|
||||
|
|
@ -2011,6 +2060,8 @@
|
|||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
|
|
|||
|
|
@ -72,18 +72,19 @@
|
|||
"zustand": "^5.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4"
|
||||
"vite": "^7.2.4",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,17 @@ import {
|
|||
import { MODEL_TYPE_TO_HF_TASK } from "@/config/training";
|
||||
import {
|
||||
useDebouncedValue,
|
||||
useGpuInfo,
|
||||
useHfModelSearch,
|
||||
useInfiniteScroll,
|
||||
} from "@/hooks";
|
||||
import { formatCompact } from "@/lib/utils";
|
||||
import {
|
||||
type TrainingMethod as VramTrainingMethod,
|
||||
type VramFitStatus,
|
||||
checkVramFit,
|
||||
estimateLoadingVram,
|
||||
} from "@/lib/vram";
|
||||
import { useTrainingConfigStore } from "@/features/training";
|
||||
import type { TrainingMethod } from "@/types/training";
|
||||
import {
|
||||
|
|
@ -57,6 +64,8 @@ const DARK_CONTENT =
|
|||
"bg-foreground text-background shadow-xl border-background/10 [--accent:rgba(255,255,255,0.1)] [--accent-foreground:white] [&_[data-slot=select-item]]:text-white/70 [&_[data-slot=select-scroll-up-button]]:bg-foreground [&_[data-slot=select-scroll-down-button]]:bg-foreground";
|
||||
|
||||
export function ModelSection() {
|
||||
const gpu = useGpuInfo();
|
||||
|
||||
const {
|
||||
modelType,
|
||||
selectedModel,
|
||||
|
|
@ -122,6 +131,37 @@ export function ModelSection() {
|
|||
return ids;
|
||||
}, [hfResults, selectedModel]);
|
||||
|
||||
// Pre-compute VRAM fit status for every model in the current result set.
|
||||
// Keyed by model id so the render callback is a simple O(1) lookup.
|
||||
//
|
||||
// Pre-compute VRAM fit status for every model in the current result set.
|
||||
// Keyed by model id so the render callback is a simple O(1) lookup.
|
||||
// Re-computes when the training method changes (QLoRA=4-bit vs LoRA/Full=fp16).
|
||||
const vramMap = useMemo(() => {
|
||||
const method = trainingMethod as VramTrainingMethod;
|
||||
const map = new Map<
|
||||
string,
|
||||
{ est: number; status: VramFitStatus | null; detail: string | null }
|
||||
>();
|
||||
for (const r of hfResults) {
|
||||
const detail = r.totalParams
|
||||
? formatCompact(r.totalParams)
|
||||
: r.downloads != null
|
||||
? `\u2193${formatCompact(r.downloads)}`
|
||||
: null;
|
||||
if (r.totalParams) {
|
||||
const est = estimateLoadingVram(r.totalParams, method);
|
||||
const status = gpu.available
|
||||
? checkVramFit(est, gpu.memoryTotalGb)
|
||||
: null;
|
||||
map.set(r.id, { est, status, detail });
|
||||
} else {
|
||||
map.set(r.id, { est: 0, status: null, detail });
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [hfResults, gpu, trainingMethod]);
|
||||
|
||||
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollRef, sentinelRef } = useInfiniteScroll(
|
||||
fetchMore,
|
||||
|
|
@ -225,21 +265,21 @@ export function ModelSection() {
|
|||
>
|
||||
<ComboboxList className="p-1 !max-h-none !overflow-visible">
|
||||
{(id: string) => {
|
||||
const r = hfResults.find((m) => m.id === id);
|
||||
const detail = r?.totalParams
|
||||
? formatCompact(r.totalParams)
|
||||
: r?.downloads != null
|
||||
? `↓${formatCompact(r.downloads)}`
|
||||
: null;
|
||||
const entry = vramMap.get(id);
|
||||
const detail = entry?.detail ?? null;
|
||||
const fitStatus = entry?.status ?? null;
|
||||
const vramEst = entry?.est ?? null;
|
||||
const exceeds = fitStatus === "exceeds";
|
||||
|
||||
return (
|
||||
<ComboboxItem
|
||||
key={id}
|
||||
value={id}
|
||||
className="justify-between"
|
||||
className={`justify-between ${exceeds ? "opacity-50" : ""}`}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
<span className={`min-w-0 flex-1 truncate ${exceeds ? "line-through decoration-muted-foreground/50" : ""}`}>
|
||||
{id}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -248,13 +288,34 @@ export function ModelSection() {
|
|||
className="max-w-xs break-all"
|
||||
>
|
||||
{id}
|
||||
{vramEst != null && vramEst > 0 && gpu.available && (
|
||||
<span className="block text-[10px] mt-1">
|
||||
{exceeds
|
||||
? `Needs ~${vramEst}GB VRAM (GPU: ${gpu.memoryTotalGb}GB)`
|
||||
: fitStatus === "tight"
|
||||
? `~${vramEst}GB VRAM (tight fit on ${gpu.memoryTotalGb}GB)`
|
||||
: `~${vramEst}GB VRAM`}
|
||||
</span>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{detail && (
|
||||
<span className="text-[10px] text-muted-foreground shrink-0">
|
||||
{detail}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1.5 shrink-0">
|
||||
{fitStatus === "exceeds" && (
|
||||
<span className="text-[9px] font-medium text-red-400">
|
||||
OOM
|
||||
</span>
|
||||
)}
|
||||
{fitStatus === "tight" && (
|
||||
<span className="text-[9px] font-medium text-amber-400">
|
||||
TIGHT
|
||||
</span>
|
||||
)}
|
||||
{detail && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{detail}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</ComboboxItem>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export { useDebouncedValue } from "./use-debounced-value";
|
||||
export { useGpuInfo } from "./use-gpu-info";
|
||||
export { useHfModelSearch } from "./use-hf-model-search";
|
||||
export { useHfDatasetSearch } from "./use-hf-dataset-search";
|
||||
export { useInfiniteScroll } from "./use-infinite-scroll";
|
||||
|
|
|
|||
68
studio/frontend/src/hooks/use-gpu-info.ts
Normal file
68
studio/frontend/src/hooks/use-gpu-info.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface GpuInfo {
|
||||
available: boolean;
|
||||
name: string;
|
||||
memoryTotalGb: number;
|
||||
}
|
||||
|
||||
const DEFAULT_GPU: GpuInfo = {
|
||||
available: false,
|
||||
name: "Unknown",
|
||||
memoryTotalGb: 0,
|
||||
};
|
||||
|
||||
// Module-level cache so multiple components share one fetch.
|
||||
let cachedGpu: GpuInfo | null = null;
|
||||
let fetchPromise: Promise<GpuInfo> | null = null;
|
||||
|
||||
async function fetchGpuOnce(): Promise<GpuInfo> {
|
||||
if (cachedGpu) return cachedGpu;
|
||||
if (fetchPromise) return fetchPromise;
|
||||
|
||||
fetchPromise = (async () => {
|
||||
try {
|
||||
const res = await fetch("/api/system");
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
const gpuData = data?.gpu;
|
||||
if (!gpuData?.available || !gpuData.devices?.length) return DEFAULT_GPU;
|
||||
const dev = gpuData.devices[0];
|
||||
const info: GpuInfo = {
|
||||
available: true,
|
||||
name: dev.name ?? "Unknown",
|
||||
memoryTotalGb: dev.memory_total_gb ?? 0,
|
||||
};
|
||||
cachedGpu = info;
|
||||
return info;
|
||||
} catch {
|
||||
// Reset promise so subsequent calls retry (e.g. backend wasn't ready)
|
||||
fetchPromise = null;
|
||||
return DEFAULT_GPU;
|
||||
}
|
||||
})();
|
||||
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch GPU info from the backend /api/system endpoint.
|
||||
*
|
||||
* The result is cached at module level -- only one network request is made
|
||||
* regardless of how many components call this hook.
|
||||
*/
|
||||
export function useGpuInfo(): GpuInfo {
|
||||
const [gpu, setGpu] = useState<GpuInfo>(cachedGpu ?? DEFAULT_GPU);
|
||||
|
||||
useEffect(() => {
|
||||
if (cachedGpu) return;
|
||||
|
||||
let cancelled = false;
|
||||
fetchGpuOnce().then((info) => {
|
||||
if (!cancelled) setGpu(info);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
return gpu;
|
||||
}
|
||||
123
studio/frontend/src/lib/vram.test.ts
Normal file
123
studio/frontend/src/lib/vram.test.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
BNB_4BIT_LOADING_BYTES,
|
||||
FP16_LOADING_BYTES,
|
||||
LOADING_OVERHEAD_GB,
|
||||
checkVramFit,
|
||||
estimateLoadingVram,
|
||||
} from "./vram";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// estimateLoadingVram (QLoRA / 4-bit -- default)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("estimateLoadingVram (qlora)", () => {
|
||||
it("returns overhead only for 0 params", () => {
|
||||
expect(estimateLoadingVram(0)).toBe(LOADING_OVERHEAD_GB);
|
||||
});
|
||||
|
||||
it("Qwen2.5-0.5B (0.49B params)", () => {
|
||||
const est = estimateLoadingVram(0.49e9);
|
||||
// 0.49 * 0.9 + 1.4 = 1.841 -> rounds to 1.8
|
||||
expect(est).toBeCloseTo(1.8, 1);
|
||||
});
|
||||
|
||||
it("Llama-3.2-1B (1.24B params)", () => {
|
||||
const est = estimateLoadingVram(1.24e9);
|
||||
// 1.24 * 0.9 + 1.4 = 2.516 -> rounds to 2.5
|
||||
expect(est).toBeCloseTo(2.5, 1);
|
||||
});
|
||||
|
||||
it("Llama-3.2-3B (3.21B params)", () => {
|
||||
const est = estimateLoadingVram(3.21e9);
|
||||
// 3.21 * 0.9 + 1.4 = 4.289 -> rounds to 4.3
|
||||
expect(est).toBeCloseTo(4.3, 1);
|
||||
});
|
||||
|
||||
it("Llama-3.1-8B (8.03B params)", () => {
|
||||
const est = estimateLoadingVram(8.03e9);
|
||||
// 8.03 * 0.9 + 1.4 = 8.627 -> rounds to 8.6
|
||||
expect(est).toBeCloseTo(8.6, 1);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// estimateLoadingVram (LoRA / fp16)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("estimateLoadingVram (lora / fp16)", () => {
|
||||
it("returns overhead only for 0 params", () => {
|
||||
expect(estimateLoadingVram(0, "lora")).toBe(LOADING_OVERHEAD_GB);
|
||||
});
|
||||
|
||||
it("1B model at fp16", () => {
|
||||
const est = estimateLoadingVram(1e9, "lora");
|
||||
// 1.0 * 2.0 + 1.4 = 3.4
|
||||
expect(est).toBeCloseTo(3.4, 1);
|
||||
});
|
||||
|
||||
it("8B model at fp16", () => {
|
||||
const est = estimateLoadingVram(8e9, "lora");
|
||||
// 8.0 * 2.0 + 1.4 = 17.4
|
||||
expect(est).toBeCloseTo(17.4, 1);
|
||||
});
|
||||
|
||||
it("full fine-tune uses same fp16 rate", () => {
|
||||
const full = estimateLoadingVram(3e9, "full");
|
||||
const lora = estimateLoadingVram(3e9, "lora");
|
||||
expect(full).toBe(lora);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sanity: fp16 always requires more VRAM than 4-bit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("fp16 vs 4-bit ordering", () => {
|
||||
it("fp16 estimate is always larger for non-zero params", () => {
|
||||
for (const params of [0.5e9, 1e9, 3e9, 7e9, 13e9]) {
|
||||
const q4 = estimateLoadingVram(params, "qlora");
|
||||
const fp16 = estimateLoadingVram(params, "lora");
|
||||
expect(fp16).toBeGreaterThan(q4);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// checkVramFit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("checkVramFit", () => {
|
||||
it("fits when ratio <= 0.75", () => {
|
||||
expect(checkVramFit(6, 16)).toBe("fits");
|
||||
expect(checkVramFit(12, 16)).toBe("fits");
|
||||
});
|
||||
|
||||
it("tight when ratio 0.75..1.0", () => {
|
||||
expect(checkVramFit(13, 16)).toBe("tight");
|
||||
expect(checkVramFit(16, 16)).toBe("tight");
|
||||
});
|
||||
|
||||
it("exceeds when ratio > 1.0", () => {
|
||||
expect(checkVramFit(17, 16)).toBe("exceeds");
|
||||
});
|
||||
|
||||
it("handles 0 available gracefully", () => {
|
||||
expect(checkVramFit(5, 0)).toBe("exceeds");
|
||||
expect(checkVramFit(0, 0)).toBe("fits");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants sanity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("constants", () => {
|
||||
it("4-bit rate is less than fp16 rate", () => {
|
||||
expect(BNB_4BIT_LOADING_BYTES).toBeLessThan(FP16_LOADING_BYTES);
|
||||
});
|
||||
|
||||
it("overhead is positive", () => {
|
||||
expect(LOADING_OVERHEAD_GB).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
95
studio/frontend/src/lib/vram.ts
Normal file
95
studio/frontend/src/lib/vram.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* VRAM estimation for model loading (4-bit quantization via bitsandbytes).
|
||||
*
|
||||
* Estimates the total driver-level VRAM (what nvidia-smi reports) needed to
|
||||
* load a model in 4-bit with Unsloth / bitsandbytes. This determines
|
||||
* whether a model will fit on the user's GPU before any training begins.
|
||||
*
|
||||
* Formula: totalParams * 0.90 + 1.4 GB
|
||||
*
|
||||
* Calibrated against isolated Unsloth model loads on RTX 5070 Ti (2026.2):
|
||||
* Qwen2.5-0.5B (0.49B) : est 1.8 vs actual 1.86 GB (-3%)
|
||||
* Llama-3.2-1B (1.24B) : est 2.5 vs actual 2.54 GB (-1%)
|
||||
* Llama-3.2-3B (3.21B) : est 4.3 vs actual 4.40 GB (-2%)
|
||||
* Llama-3.1-8B (8.03B) : est 8.6 vs actual 8.14 GB (+6%)
|
||||
*
|
||||
* Accuracy: within 3% for 0.5B-3B models, within 6% for 8B.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants (exported for testing)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Effective bytes per parameter for 4-bit model weights at driver level.
|
||||
*
|
||||
* Raw bnb 4-bit is ~0.5 bytes/param, but embedding and lm_head layers remain
|
||||
* in fp16 and bnb adds per-block quantization metadata, bringing the
|
||||
* effective rate to ~0.84-0.93 across tested architectures. 0.9 is the
|
||||
* calibrated middle ground.
|
||||
*/
|
||||
export const BNB_4BIT_LOADING_BYTES = 0.9;
|
||||
|
||||
/**
|
||||
* Fixed overhead (GB) for the CUDA driver context and PyTorch runtime.
|
||||
*
|
||||
* This is independent of model size -- it is the baseline GPU memory consumed
|
||||
* before any model weights are loaded. Measured at 1.34-1.46 GB across
|
||||
* tested models; we use 1.4 as the default.
|
||||
*/
|
||||
export const LOADING_OVERHEAD_GB = 1.4;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Estimation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type VramFitStatus = "fits" | "tight" | "exceeds";
|
||||
|
||||
/**
|
||||
* Bytes per parameter when loading a model at fp16/bf16 (LoRA, full FT).
|
||||
*
|
||||
* This is the theoretical value (2 bytes = 16 bits). Not yet calibrated
|
||||
* against actual measurements -- the real driver-level usage may be slightly
|
||||
* higher due to buffers and metadata, similar to how 4-bit is 0.9 vs 0.5.
|
||||
*/
|
||||
export const FP16_LOADING_BYTES = 2.0;
|
||||
|
||||
export type TrainingMethod = "qlora" | "lora" | "full";
|
||||
|
||||
/**
|
||||
* Estimate VRAM (GB) needed to load a model with Unsloth.
|
||||
*
|
||||
* The bytes-per-param rate depends on the training method:
|
||||
* - QLoRA : 4-bit quantized via bnb -> 0.90 bytes/param (calibrated)
|
||||
* - LoRA : fp16 -> 2.0 bytes/param (theoretical)
|
||||
* - Full : fp16 -> 2.0 bytes/param (theoretical)
|
||||
*
|
||||
* Formula: totalParams * bytesPerParam + 1.4 GB overhead
|
||||
*/
|
||||
export function estimateLoadingVram(
|
||||
totalParams: number,
|
||||
method: TrainingMethod = "qlora",
|
||||
): number {
|
||||
const bytesPerParam =
|
||||
method === "qlora" ? BNB_4BIT_LOADING_BYTES : FP16_LOADING_BYTES;
|
||||
const gb = (totalParams / 1e9) * bytesPerParam + LOADING_OVERHEAD_GB;
|
||||
return Math.round(gb * 10) / 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a model fits in the available GPU VRAM.
|
||||
*
|
||||
* fits - uses <= 75% of available
|
||||
* tight - uses 75-100% of available
|
||||
* exceeds - uses > 100% of available
|
||||
*/
|
||||
export function checkVramFit(
|
||||
requiredGb: number,
|
||||
availableGb: number,
|
||||
): VramFitStatus {
|
||||
if (availableGb <= 0) return requiredGb <= 0 ? "fits" : "exceeds";
|
||||
const ratio = requiredGb / availableGb;
|
||||
if (ratio <= 0.75) return "fits";
|
||||
if (ratio <= 1.0) return "tight";
|
||||
return "exceeds";
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue