Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce67529524 | |||
| c3f3d0eeb9 | |||
| 7199e556ef | |||
| f376c24895 | |||
| d63277edbd | |||
| dd121ff966 |
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Starting overnight bench runs at $(date) ==="
|
||||
|
||||
echo "--- [1/8] Qwen3.5-27B-GPTQ-Int4 ---"
|
||||
uv run bench/exo_bench.py --model "mlx-community/Qwen3.5-27B-GPTQ-Int4" --pp 700 --tg 32067,33085 --repeat 1
|
||||
|
||||
echo "--- [2/8] NVIDIA-Nemotron-3-Nano-30B-A3B (1120,1330,23100) ---"
|
||||
uv run bench/exo_bench.py --model "mlx-community/NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-BF16" --pp 700 --tg 1120,1330,23100 --repeat 1
|
||||
|
||||
echo "--- [3/8] Qwen3.5-35B-A3B-bf16 ---"
|
||||
uv run bench/exo_bench.py --model "mlx-community/Qwen3.5-35B-A3B-bf16" --pp 700 --tg 6200,6450,25600,38000 --repeat 1
|
||||
|
||||
echo "--- [4/8] Qwen3.5-122B-A10B-GPTQ-Int4 ---"
|
||||
uv run bench/exo_bench.py --model "Qwen/Qwen3.5-122B-A10B-GPTQ-Int4" --pp 700 --tg 36000 --repeat 1
|
||||
|
||||
echo "--- [5/8] Qwen3.5-27B-FP8 ---"
|
||||
uv run bench/exo_bench.py --model "Qwen/Qwen3.5-27B-FP8" --pp 700 --tg 33736,35133 --repeat 1
|
||||
|
||||
echo "--- [6/8] GLM-4.7-Flash-bf16 ---"
|
||||
uv run bench/exo_bench.py --model "mlx-community/GLM-4.7-Flash-bf16" --pp 700 --tg 29000 --repeat 1
|
||||
|
||||
echo "--- [7/8] NVIDIA-Nemotron-3-Nano-30B-A3B (23000,1200) ---"
|
||||
uv run bench/exo_bench.py --model "mlx-community/NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-BF16" --pp 700 --tg 23000,1200 --repeat 1
|
||||
|
||||
echo "--- [8/8] Qwen3.5-27B-bf16 ---"
|
||||
uv run bench/exo_bench.py --model "mlx-community/Qwen3.5-27B-bf16" --pp 700 --tg 32000,35400 --repeat 1
|
||||
|
||||
echo "=== All bench runs complete at $(date) ==="
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
/** "macbook pro" | "mac studio" | "mac mini" etc. */
|
||||
/** "macbook pro" | "mac studio" | "mac mini" | "dgx spark" | "linux" etc. */
|
||||
deviceType: string;
|
||||
/** Center X coordinate in SVG space */
|
||||
cx: number;
|
||||
@@ -38,10 +38,43 @@
|
||||
const LOGO_NATIVE_WIDTH = 814;
|
||||
const LOGO_NATIVE_HEIGHT = 1000;
|
||||
|
||||
// NVIDIA logo SVG path
|
||||
const NVIDIA_LOGO_PATH =
|
||||
"M0.81 0.429V0.299c0.013 -0.001 0.026 -0.002 0.038 -0.002 0.355 -0.011 0.588 0.306 0.588 0.306S1.186 0.952 0.916 0.952c-0.036 0 -0.071 -0.006 -0.105 -0.017V0.542c0.138 0.017 0.166 0.078 0.249 0.216l0.185 -0.155s-0.135 -0.177 -0.362 -0.177c-0.024 -0.001 -0.048 0.001 -0.072 0.003m0 -0.429v0.194l0.038 -0.002c0.494 -0.017 0.816 0.405 0.816 0.405s-0.37 0.45 -0.754 0.45c-0.034 0 -0.066 -0.003 -0.099 -0.009v0.12c0.027 0.003 0.055 0.006 0.082 0.006 0.358 0 0.618 -0.183 0.869 -0.399 0.042 0.034 0.212 0.114 0.247 0.15 -0.238 0.2 -0.794 0.361 -1.11 0.361 -0.03 0 -0.059 -0.002 -0.088 -0.005v0.169h1.362V0zm0 0.935v0.102c-0.331 -0.059 -0.423 -0.404 -0.423 -0.404s0.159 -0.176 0.423 -0.205v0.112h-0.001C0.671 0.524 0.562 0.654 0.562 0.654s0.062 0.218 0.248 0.282m-0.588 -0.316s0.196 -0.29 0.589 -0.32V0.194C0.376 0.229 0 0.597 0 0.597s0.213 0.616 0.81 0.672v-0.112c-0.438 -0.054 -0.588 -0.538 -0.588 -0.538";
|
||||
|
||||
const wireColor = "rgba(179,179,179,0.8)";
|
||||
const strokeWidth = 1.5;
|
||||
|
||||
const modelLower = $derived(deviceType.toLowerCase());
|
||||
const isSpark = $derived(
|
||||
modelLower.includes("dgx") || modelLower.includes("gx10"),
|
||||
);
|
||||
const isLinux = $derived(!isSpark && modelLower.startsWith("linux"));
|
||||
const isLinuxLaptop = $derived(isLinux && modelLower.includes("laptop"));
|
||||
|
||||
// ── DGX Spark dimensions ──
|
||||
const dgxW = $derived(size * 1.55);
|
||||
const dgxH = $derived(size * 0.58);
|
||||
const dgxX = $derived(cx - dgxW / 2);
|
||||
const dgxY = $derived(cy - dgxH / 2);
|
||||
const dgxChassisX = $derived(dgxX - dgxW * 0.03);
|
||||
const dgxChassisW = $derived(dgxW * 1.05);
|
||||
const dgxHandleW = $derived(dgxW * 0.27);
|
||||
const dgxHandleGap = $derived(dgxH * 0.05);
|
||||
const dgxHandleH = $derived(dgxH - dgxHandleGap * 2);
|
||||
const dgxHandleY = $derived(dgxY + dgxHandleGap);
|
||||
const dgxInnerHandleW = $derived(dgxW * 0.12);
|
||||
const dgxInnerHandleH = $derived(dgxHandleH - dgxH * 0.06);
|
||||
const dgxLeftHandleX = $derived(dgxX + 4);
|
||||
const dgxRightHandleX = $derived(dgxX + dgxW - dgxHandleW - 4);
|
||||
const dgxClipId = $derived(`di-dgx-${uid}`);
|
||||
const dgxTextureId = $derived(`di-dgx-tex-${uid}`);
|
||||
|
||||
// ── Linux Desktop dimensions (reuses Mac Studio proportions) ──
|
||||
const linuxDesktopClipId = $derived(`di-linux-desktop-${uid}`);
|
||||
|
||||
// ── Linux Laptop dimensions (reuses MacBook proportions) ──
|
||||
const linuxScreenClipId = $derived(`di-linux-screen-${uid}`);
|
||||
|
||||
// ── Mac Studio dimensions (same ratios as TopologyGraph) ──
|
||||
const studioW = $derived(size * 1.25);
|
||||
@@ -114,7 +147,264 @@
|
||||
const studioClipId = $derived(`di-studio-${uid}`);
|
||||
</script>
|
||||
|
||||
{#if modelLower === "mac studio" || modelLower === "mac mini"}
|
||||
{#if isSpark}
|
||||
<!-- DGX Spark -->
|
||||
<defs>
|
||||
<clipPath id={dgxClipId}>
|
||||
<rect x={dgxX} y={dgxY} width={dgxW} height={dgxH} rx="3" />
|
||||
</clipPath>
|
||||
<pattern
|
||||
id={dgxTextureId}
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="8"
|
||||
height="8"
|
||||
>
|
||||
<rect width="8" height="8" fill="#6f6248" />
|
||||
<circle cx="2" cy="2" r="1" fill="#5a4f3b" opacity="0.5" />
|
||||
<circle cx="6" cy="6" r="1" fill="#4a4232" opacity="0.45" />
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- Main body -->
|
||||
<rect
|
||||
x={dgxChassisX}
|
||||
y={dgxY}
|
||||
width={dgxChassisW}
|
||||
height={dgxH}
|
||||
rx="3"
|
||||
fill="url(#{dgxTextureId})"
|
||||
stroke={wireColor}
|
||||
stroke-width={strokeWidth}
|
||||
/>
|
||||
|
||||
<!-- Side border accents -->
|
||||
<rect
|
||||
x={dgxChassisX}
|
||||
y={dgxY}
|
||||
width={dgxW * 0.02}
|
||||
height={dgxH}
|
||||
fill="#8a7a56"
|
||||
/>
|
||||
<rect
|
||||
x={dgxChassisX + dgxChassisW - dgxW * 0.02}
|
||||
y={dgxY}
|
||||
width={dgxW * 0.02}
|
||||
height={dgxH}
|
||||
fill="#8a7a56"
|
||||
/>
|
||||
|
||||
<!-- Memory fill -->
|
||||
{#if ramPercent > 0}
|
||||
<rect
|
||||
x={dgxX}
|
||||
y={dgxY + dgxH - (ramPercent / 100) * dgxH}
|
||||
width={dgxW}
|
||||
height={(ramPercent / 100) * dgxH}
|
||||
fill="rgba(255,215,0,0.45)"
|
||||
clip-path="url(#{dgxClipId})"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Left handle -->
|
||||
<rect
|
||||
x={dgxLeftHandleX}
|
||||
y={dgxHandleY}
|
||||
width={dgxHandleW}
|
||||
height={dgxHandleH}
|
||||
rx="2.4"
|
||||
fill="#b3a170"
|
||||
stroke="#403723"
|
||||
stroke-width="0.7"
|
||||
/>
|
||||
<rect
|
||||
x={dgxLeftHandleX + dgxHandleW * 0.06}
|
||||
y={dgxHandleY + dgxH * 0.03}
|
||||
width={dgxInnerHandleW}
|
||||
height={dgxInnerHandleH}
|
||||
rx="1.6"
|
||||
fill="#8a7a56"
|
||||
/>
|
||||
|
||||
<!-- Right handle -->
|
||||
<rect
|
||||
x={dgxRightHandleX}
|
||||
y={dgxHandleY}
|
||||
width={dgxHandleW}
|
||||
height={dgxHandleH}
|
||||
rx="2.4"
|
||||
fill="#b3a170"
|
||||
stroke="#403723"
|
||||
stroke-width="0.7"
|
||||
/>
|
||||
<rect
|
||||
x={dgxRightHandleX + dgxHandleW - dgxInnerHandleW - dgxHandleW * 0.08}
|
||||
y={dgxHandleY + dgxH * 0.03}
|
||||
width={dgxInnerHandleW}
|
||||
height={dgxInnerHandleH}
|
||||
rx="1.6"
|
||||
fill="#8a7a56"
|
||||
/>
|
||||
|
||||
<!-- NVIDIA logo (rotated 90deg on left handle) -->
|
||||
{@const badgeW = dgxW * 0.09}
|
||||
{@const badgeH = dgxHandleH * 0.5}
|
||||
{@const badgeX = dgxLeftHandleX + dgxHandleW - badgeW - dgxHandleW * 0.06}
|
||||
{@const badgeYPos = dgxHandleY + (dgxHandleH - badgeH) / 2}
|
||||
{@const textSz = badgeW * 0.58}
|
||||
{@const logoW = textSz * 1.2}
|
||||
{@const logoH = logoW * (1.438 / 2.174)}
|
||||
{@const ctrX = badgeX + badgeW / 2 - badgeW * 0.03}
|
||||
{@const ctrY = badgeYPos + badgeH / 2}
|
||||
{@const labelGap = badgeW * 0.15}
|
||||
{@const totalW = logoW + labelGap + textSz * 3.6}
|
||||
<g transform="rotate(90 {ctrX} {ctrY})">
|
||||
<svg
|
||||
x={ctrX - totalW / 2}
|
||||
y={ctrY - logoH / 2}
|
||||
width={logoW}
|
||||
height={logoH}
|
||||
viewBox="0 0 2.174 1.438"
|
||||
>
|
||||
<path d={NVIDIA_LOGO_PATH} fill="#76b900" />
|
||||
</svg>
|
||||
<text
|
||||
x={ctrX - totalW / 2 + logoW + labelGap}
|
||||
y={ctrY}
|
||||
text-anchor="start"
|
||||
dominant-baseline="middle"
|
||||
fill="#8a7a56"
|
||||
font-size={textSz}
|
||||
font-family="monospace"
|
||||
font-weight="700">NVIDIA</text
|
||||
>
|
||||
</g>
|
||||
{:else if isLinuxLaptop}
|
||||
<!-- Linux Laptop — MacBook shape with Tux logo -->
|
||||
<defs>
|
||||
<clipPath id={linuxScreenClipId}>
|
||||
<rect
|
||||
x={mbScreenX + mbBezel}
|
||||
y={mbY + mbBezel}
|
||||
width={mbScreenW - mbBezel * 2}
|
||||
height={mbScreenH - mbBezel * 2}
|
||||
rx="2"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect
|
||||
x={mbScreenX}
|
||||
y={mbY}
|
||||
width={mbScreenW}
|
||||
height={mbScreenH}
|
||||
rx="3"
|
||||
fill="#1a1a1a"
|
||||
stroke={wireColor}
|
||||
stroke-width={strokeWidth}
|
||||
/>
|
||||
<rect
|
||||
x={mbScreenX + mbBezel}
|
||||
y={mbY + mbBezel}
|
||||
width={mbScreenW - mbBezel * 2}
|
||||
height={mbScreenH - mbBezel * 2}
|
||||
rx="2"
|
||||
fill="#0a0a12"
|
||||
/>
|
||||
{#if ramPercent > 0}
|
||||
<rect
|
||||
x={mbScreenX + mbBezel}
|
||||
y={mbY + mbBezel + (mbMemTotalH - mbMemH)}
|
||||
width={mbScreenW - mbBezel * 2}
|
||||
height={mbMemH}
|
||||
fill="rgba(255,215,0,0.85)"
|
||||
clip-path="url(#{linuxScreenClipId})"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Terminal prompt on screen -->
|
||||
<text
|
||||
x={cx}
|
||||
y={mbY + mbScreenH / 2}
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
fill="#FFFFFF"
|
||||
opacity="0.9"
|
||||
font-size={mbScreenH * 0.25}
|
||||
font-family="SF Mono, Monaco, monospace"
|
||||
font-weight="700">{">_"}</text
|
||||
>
|
||||
|
||||
<path
|
||||
d="M {mbBaseTopX} {mbBaseY} L {mbBaseTopX +
|
||||
mbBaseTopW} {mbBaseY} L {mbBaseBottomX + mbBaseBottomW} {mbBaseY +
|
||||
mbBaseH} L {mbBaseBottomX} {mbBaseY + mbBaseH} Z"
|
||||
fill="#2c2c2c"
|
||||
stroke={wireColor}
|
||||
stroke-width="1"
|
||||
/>
|
||||
<rect
|
||||
x={mbKbX}
|
||||
y={mbKbY}
|
||||
width={mbKbW}
|
||||
height={mbKbH}
|
||||
fill="rgba(0,0,0,0.2)"
|
||||
rx="2"
|
||||
/>
|
||||
<rect
|
||||
x={mbTpX}
|
||||
y={mbTpY}
|
||||
width={mbTpW}
|
||||
height={mbTpH}
|
||||
fill="rgba(255,255,255,0.08)"
|
||||
rx="2"
|
||||
/>
|
||||
{:else if isLinux}
|
||||
<!-- Linux Desktop — Mac Studio shape with Tux logo -->
|
||||
<defs>
|
||||
<clipPath id={linuxDesktopClipId}>
|
||||
<rect
|
||||
x={studioX}
|
||||
y={studioY + studioTopH}
|
||||
width={studioW}
|
||||
height={studioH - studioTopH}
|
||||
rx={studioCorner - 1}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect
|
||||
x={studioX}
|
||||
y={studioY}
|
||||
width={studioW}
|
||||
height={studioH}
|
||||
rx={studioCorner}
|
||||
fill="#1a1a1a"
|
||||
stroke={wireColor}
|
||||
stroke-width={strokeWidth}
|
||||
/>
|
||||
{#if ramPercent > 0}
|
||||
<rect
|
||||
x={studioX}
|
||||
y={studioY + studioTopH + (studioMemTotalH - studioMemH)}
|
||||
width={studioW}
|
||||
height={studioMemH}
|
||||
fill="rgba(255,215,0,0.75)"
|
||||
clip-path="url(#{linuxDesktopClipId})"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Terminal prompt on front face -->
|
||||
<text
|
||||
x={cx}
|
||||
y={studioY + studioTopH + (studioH - studioTopH) / 2}
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
fill="rgba(255,255,255,0.5)"
|
||||
font-size={(studioH - studioTopH) * 0.4}
|
||||
font-family="SF Mono, Monaco, monospace"
|
||||
font-weight="700">{">_"}</text
|
||||
>
|
||||
{:else if modelLower === "mac studio" || modelLower === "mac mini"}
|
||||
<!-- Mac Studio / Mac Mini -->
|
||||
<defs>
|
||||
<clipPath id={studioClipId}>
|
||||
|
||||
@@ -169,8 +169,10 @@
|
||||
|
||||
function getDeviceType(
|
||||
name: string,
|
||||
): "macbook" | "studio" | "mini" | "unknown" {
|
||||
): "macbook" | "studio" | "mini" | "dgx" | "linux" | "unknown" {
|
||||
const lower = name.toLowerCase();
|
||||
if (lower.includes("dgx") || lower.includes("gx10")) return "dgx";
|
||||
if (lower.includes("linux")) return "linux";
|
||||
if (lower.includes("macbook")) return "macbook";
|
||||
if (lower.includes("studio")) return "studio";
|
||||
if (lower.includes("mini")) return "mini";
|
||||
@@ -278,7 +280,7 @@
|
||||
let placementNodes: Array<{
|
||||
id: string;
|
||||
deviceName: string;
|
||||
deviceType: "macbook" | "studio" | "mini" | "unknown";
|
||||
deviceType: "macbook" | "studio" | "mini" | "dgx" | "linux" | "unknown";
|
||||
totalGB: number;
|
||||
currentUsedGB: number;
|
||||
modelUsageGB: number;
|
||||
@@ -968,6 +970,137 @@
|
||||
/>
|
||||
{/if}
|
||||
</g>
|
||||
{:else if node.deviceType === "dgx"}
|
||||
<!-- DGX Spark icon -->
|
||||
{@const s = node.iconSize}
|
||||
{@const dgxW = s * 1.4}
|
||||
{@const dgxH = s * 0.52}
|
||||
<g transform="translate({-dgxW / 2}, {-dgxH / 2})">
|
||||
<!-- Chassis -->
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width={dgxW}
|
||||
height={dgxH}
|
||||
rx="2"
|
||||
fill="#6f6248"
|
||||
stroke={node.isUsed ? "#FFD700" : "#4B5563"}
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<!-- Side accents -->
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width={dgxW * 0.02}
|
||||
height={dgxH}
|
||||
fill="#8a7a56"
|
||||
/>
|
||||
<rect
|
||||
x={dgxW - dgxW * 0.02}
|
||||
y="0"
|
||||
width={dgxW * 0.02}
|
||||
height={dgxH}
|
||||
fill="#8a7a56"
|
||||
/>
|
||||
<!-- Left handle -->
|
||||
<rect
|
||||
x={dgxW * 0.04}
|
||||
y={dgxH * 0.08}
|
||||
width={dgxW * 0.22}
|
||||
height={dgxH * 0.84}
|
||||
rx="2"
|
||||
fill="#b3a170"
|
||||
stroke="#403723"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<!-- Right handle -->
|
||||
<rect
|
||||
x={dgxW - dgxW * 0.04 - dgxW * 0.22}
|
||||
y={dgxH * 0.08}
|
||||
width={dgxW * 0.22}
|
||||
height={dgxH * 0.84}
|
||||
rx="2"
|
||||
fill="#b3a170"
|
||||
stroke="#403723"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<!-- Memory fill -->
|
||||
<rect
|
||||
x="2"
|
||||
y={dgxH - dgxH * (node.currentPercent / 100)}
|
||||
width={dgxW - 4}
|
||||
height={dgxH * (node.currentPercent / 100)}
|
||||
fill="rgba(255,215,0,0.35)"
|
||||
/>
|
||||
{#if node.modelUsageGB > 0 && node.isUsed}
|
||||
<rect
|
||||
x="2"
|
||||
y={dgxH - dgxH * (node.newPercent / 100)}
|
||||
width={dgxW - 4}
|
||||
height={dgxH *
|
||||
((node.newPercent - node.currentPercent) / 100)}
|
||||
fill="#FFD700"
|
||||
filter="url(#memGlow-{filterId})"
|
||||
class="animate-pulse-slow"
|
||||
/>
|
||||
{/if}
|
||||
</g>
|
||||
{:else if node.deviceType === "linux"}
|
||||
<!-- Linux Tux penguin icon -->
|
||||
{@const sz = node.iconSize}
|
||||
{@const sc = sz / 100}
|
||||
<g transform="translate({-sz / 2}, {-sz / 2})">
|
||||
<!-- Body -->
|
||||
<path
|
||||
d="M50 8c-8 0-14 6-14 13 0 4 2 8 5 10-8 4-16 14-16 28v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V59c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z"
|
||||
transform="scale({sc})"
|
||||
fill="#1a1a1a"
|
||||
stroke={node.isUsed ? "#FFD700" : "#4B5563"}
|
||||
stroke-width={1.5 / sc}
|
||||
/>
|
||||
<!-- Belly -->
|
||||
<path
|
||||
d="M38 52c0-8 5-15 12-15s12 7 12 15v14c0 4-5 7-12 7s-12-3-12-7V52z"
|
||||
transform="scale({sc})"
|
||||
fill="rgba(220,220,220,0.85)"
|
||||
/>
|
||||
<!-- Eyes -->
|
||||
<circle cx={44 * sc} cy={16 * sc} r={2.5 * sc} fill="white" />
|
||||
<circle cx={56 * sc} cy={16 * sc} r={2.5 * sc} fill="white" />
|
||||
<circle
|
||||
cx={44 * sc}
|
||||
cy={16 * sc}
|
||||
r={1.2 * sc}
|
||||
fill="#1a1a1a"
|
||||
/>
|
||||
<circle
|
||||
cx={56 * sc}
|
||||
cy={16 * sc}
|
||||
r={1.2 * sc}
|
||||
fill="#1a1a1a"
|
||||
/>
|
||||
<!-- Beak -->
|
||||
<path
|
||||
d="M{46 * sc} {22 * sc} L{50 * sc} {27 * sc} L{54 *
|
||||
sc} {22 * sc} Z"
|
||||
fill="#E8A317"
|
||||
/>
|
||||
<!-- Feet -->
|
||||
<ellipse
|
||||
cx={42 * sc}
|
||||
cy={94 * sc}
|
||||
rx={6 * sc}
|
||||
ry={2.5 * sc}
|
||||
fill="#E8A317"
|
||||
/>
|
||||
<ellipse
|
||||
cx={58 * sc}
|
||||
cy={94 * sc}
|
||||
rx={6 * sc}
|
||||
ry={2.5 * sc}
|
||||
fill="#E8A317"
|
||||
/>
|
||||
</g>
|
||||
{:else}
|
||||
<!-- Unknown device - hexagon -->
|
||||
<g
|
||||
|
||||
@@ -117,6 +117,10 @@
|
||||
const LOGO_NATIVE_WIDTH = 814;
|
||||
const LOGO_NATIVE_HEIGHT = 1000;
|
||||
|
||||
// NVIDIA logo SVG path (from exo-nvidia)
|
||||
const NVIDIA_LOGO_PATH =
|
||||
"M0.81 0.429V0.299c0.013 -0.001 0.026 -0.002 0.038 -0.002 0.355 -0.011 0.588 0.306 0.588 0.306S1.186 0.952 0.916 0.952c-0.036 0 -0.071 -0.006 -0.105 -0.017V0.542c0.138 0.017 0.166 0.078 0.249 0.216l0.185 -0.155s-0.135 -0.177 -0.362 -0.177c-0.024 -0.001 -0.048 0.001 -0.072 0.003m0 -0.429v0.194l0.038 -0.002c0.494 -0.017 0.816 0.405 0.816 0.405s-0.37 0.45 -0.754 0.45c-0.034 0 -0.066 -0.003 -0.099 -0.009v0.12c0.027 0.003 0.055 0.006 0.082 0.006 0.358 0 0.618 -0.183 0.869 -0.399 0.042 0.034 0.212 0.114 0.247 0.15 -0.238 0.2 -0.794 0.361 -1.11 0.361 -0.03 0 -0.059 -0.002 -0.088 -0.005v0.169h1.362V0zm0 0.935v0.102c-0.331 -0.059 -0.423 -0.404 -0.423 -0.404s0.159 -0.176 0.423 -0.205v0.112h-0.001C0.671 0.524 0.562 0.654 0.562 0.654s0.062 0.218 0.248 0.282m-0.588 -0.316s0.196 -0.29 0.589 -0.32V0.194C0.376 0.229 0 0.597 0 0.597s0.213 0.616 0.81 0.672v-0.112c-0.438 -0.054 -0.588 -0.538 -0.588 -0.538";
|
||||
|
||||
function formatBytes(bytes: number, decimals = 1): string {
|
||||
if (!bytes || bytes === 0) return "0B";
|
||||
const k = 1024;
|
||||
@@ -554,6 +558,13 @@
|
||||
const clipPathId = `clip-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
||||
|
||||
const modelLower = modelId.toLowerCase();
|
||||
const identity = identitiesData[nodeInfo.id];
|
||||
const nameLower = (friendlyName || "").toLowerCase();
|
||||
const isSpark = modelLower.includes("dgx") || modelLower.includes("gx10");
|
||||
const isLinux =
|
||||
!isSpark &&
|
||||
(modelLower.startsWith("linux") || identity?.osVersion === "Linux");
|
||||
const isLinuxLaptop = isLinux && modelLower.includes("laptop");
|
||||
|
||||
// Check node states for styling
|
||||
const isHighlighted = highlightedNodes.has(nodeInfo.id);
|
||||
@@ -623,7 +634,382 @@
|
||||
`${friendlyName}\nID: ${nodeInfo.id.slice(-8)}\nMemory: ${formatBytes(ramUsed)}/${formatBytes(ramTotal)}`,
|
||||
);
|
||||
|
||||
if (modelLower === "mac studio") {
|
||||
if (isSpark) {
|
||||
// NVIDIA DGX Spark — gold chassis with textured front, side handles, and NVIDIA badge
|
||||
iconBaseWidth = nodeRadius * 1.55;
|
||||
iconBaseHeight = nodeRadius * 0.58;
|
||||
const x = nodeInfo.x - iconBaseWidth / 2;
|
||||
const y = nodeInfo.y - iconBaseHeight / 2;
|
||||
const chassisX = x - iconBaseWidth * 0.03;
|
||||
const chassisWidth = iconBaseWidth * 1.05;
|
||||
const cornerRadius = 3;
|
||||
|
||||
const dgxClipId = `dgx-clip-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
||||
defs
|
||||
.append("clipPath")
|
||||
.attr("id", dgxClipId)
|
||||
.append("rect")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("width", iconBaseWidth)
|
||||
.attr("height", iconBaseHeight)
|
||||
.attr("rx", cornerRadius);
|
||||
|
||||
// Chassis texture pattern
|
||||
const textureId = `chassis-texture-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
||||
defs
|
||||
.append("pattern")
|
||||
.attr("id", textureId)
|
||||
.attr("patternUnits", "userSpaceOnUse")
|
||||
.attr("width", 8)
|
||||
.attr("height", 8);
|
||||
const texturePattern = defs.select(`#${textureId}`);
|
||||
texturePattern
|
||||
.append("rect")
|
||||
.attr("width", 8)
|
||||
.attr("height", 8)
|
||||
.attr("fill", "#6f6248");
|
||||
texturePattern
|
||||
.append("circle")
|
||||
.attr("cx", 2)
|
||||
.attr("cy", 2)
|
||||
.attr("r", 1)
|
||||
.attr("fill", "#5a4f3b")
|
||||
.attr("opacity", 0.5);
|
||||
texturePattern
|
||||
.append("circle")
|
||||
.attr("cx", 6)
|
||||
.attr("cy", 6)
|
||||
.attr("r", 1)
|
||||
.attr("fill", "#4a4232")
|
||||
.attr("opacity", 0.45);
|
||||
|
||||
// Main body
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("class", "node-outline")
|
||||
.attr("x", chassisX)
|
||||
.attr("y", y)
|
||||
.attr("width", chassisWidth)
|
||||
.attr("height", iconBaseHeight)
|
||||
.attr("rx", cornerRadius)
|
||||
.attr("fill", `url(#${textureId})`)
|
||||
.attr("stroke", wireColor)
|
||||
.attr("stroke-width", strokeWidth);
|
||||
|
||||
// Side border accents
|
||||
const sideThickness = iconBaseWidth * 0.02;
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", chassisX)
|
||||
.attr("y", y)
|
||||
.attr("width", sideThickness)
|
||||
.attr("height", iconBaseHeight)
|
||||
.attr("fill", "#8a7a56");
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", chassisX + chassisWidth - sideThickness)
|
||||
.attr("y", y)
|
||||
.attr("width", sideThickness)
|
||||
.attr("height", iconBaseHeight)
|
||||
.attr("fill", "#8a7a56");
|
||||
|
||||
// Memory fill (bottom up)
|
||||
if (ramUsagePercent > 0) {
|
||||
const memFillHeight = (ramUsagePercent / 100) * iconBaseHeight;
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", x)
|
||||
.attr("y", y + iconBaseHeight - memFillHeight)
|
||||
.attr("width", iconBaseWidth)
|
||||
.attr("height", memFillHeight)
|
||||
.attr("fill", "rgba(255,215,0,0.45)")
|
||||
.attr("clip-path", `url(#${dgxClipId})`);
|
||||
}
|
||||
|
||||
// Side handles with inner recess
|
||||
const handleWidth = iconBaseWidth * 0.27;
|
||||
const handleGap = iconBaseHeight * 0.05;
|
||||
const handleHeight = iconBaseHeight - handleGap * 2;
|
||||
const handleY = y + handleGap;
|
||||
const innerHandleWidth = iconBaseWidth * 0.12;
|
||||
const innerHandleHeight = handleHeight - iconBaseHeight * 0.06;
|
||||
const leftHandleX = x + 4;
|
||||
const rightHandleX = x + iconBaseWidth - handleWidth - 4;
|
||||
|
||||
// Left handle
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", leftHandleX)
|
||||
.attr("y", handleY)
|
||||
.attr("width", handleWidth)
|
||||
.attr("height", handleHeight)
|
||||
.attr("rx", 2.4)
|
||||
.attr("fill", "#b3a170")
|
||||
.attr("stroke", "#403723")
|
||||
.attr("stroke-width", 0.7);
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", leftHandleX + handleWidth * 0.06)
|
||||
.attr("y", handleY + iconBaseHeight * 0.03)
|
||||
.attr("width", innerHandleWidth)
|
||||
.attr("height", innerHandleHeight)
|
||||
.attr("rx", 1.6)
|
||||
.attr("fill", "#8a7a56");
|
||||
|
||||
// Right handle
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", rightHandleX)
|
||||
.attr("y", handleY)
|
||||
.attr("width", handleWidth)
|
||||
.attr("height", handleHeight)
|
||||
.attr("rx", 2.4)
|
||||
.attr("fill", "#b3a170")
|
||||
.attr("stroke", "#403723")
|
||||
.attr("stroke-width", 0.7);
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr(
|
||||
"x",
|
||||
rightHandleX + handleWidth - innerHandleWidth - handleWidth * 0.08,
|
||||
)
|
||||
.attr("y", handleY + iconBaseHeight * 0.03)
|
||||
.attr("width", innerHandleWidth)
|
||||
.attr("height", innerHandleHeight)
|
||||
.attr("rx", 1.6)
|
||||
.attr("fill", "#8a7a56");
|
||||
|
||||
// NVIDIA logo + text label (rotated 90 deg on left handle)
|
||||
const badgeWidth = iconBaseWidth * 0.09;
|
||||
const badgeHeight = handleHeight * 0.5;
|
||||
const badgeX =
|
||||
leftHandleX + handleWidth - badgeWidth - handleWidth * 0.06;
|
||||
const badgeY = handleY + (handleHeight - badgeHeight) / 2;
|
||||
const textSize = badgeWidth * 0.58;
|
||||
const logoWidth = textSize * 1.2;
|
||||
const logoHeight = logoWidth * (1.438 / 2.174);
|
||||
const centerX = badgeX + badgeWidth / 2 - badgeWidth * 0.03;
|
||||
const centerY = badgeY + badgeHeight / 2;
|
||||
const gap = badgeWidth * 0.15;
|
||||
const totalWidth = logoWidth + gap + textSize * 3.6;
|
||||
|
||||
const labelGroup = nodeG
|
||||
.append("g")
|
||||
.attr("transform", `rotate(90 ${centerX} ${centerY})`);
|
||||
|
||||
labelGroup
|
||||
.append("svg")
|
||||
.attr("x", centerX - totalWidth / 2)
|
||||
.attr("y", centerY - logoHeight / 2)
|
||||
.attr("width", logoWidth)
|
||||
.attr("height", logoHeight)
|
||||
.attr("viewBox", "0 0 2.174 1.438")
|
||||
.append("path")
|
||||
.attr("d", NVIDIA_LOGO_PATH)
|
||||
.attr("fill", "#76b900");
|
||||
|
||||
labelGroup
|
||||
.append("text")
|
||||
.attr("x", centerX - totalWidth / 2 + logoWidth + gap)
|
||||
.attr("y", centerY)
|
||||
.attr("text-anchor", "start")
|
||||
.attr("dominant-baseline", "middle")
|
||||
.attr("fill", "#8a7a56")
|
||||
.attr("font-size", textSize)
|
||||
.attr("font-family", "monospace")
|
||||
.attr("font-weight", "700")
|
||||
.text("NVIDIA");
|
||||
} else if (isLinuxLaptop) {
|
||||
// Linux Laptop — same shape as MacBook but with Tux logo
|
||||
iconBaseWidth = nodeRadius * 1.6;
|
||||
iconBaseHeight = nodeRadius * 1.15;
|
||||
const x = nodeInfo.x - iconBaseWidth / 2;
|
||||
const y = nodeInfo.y - iconBaseHeight / 2;
|
||||
|
||||
const screenHeight = iconBaseHeight * 0.7;
|
||||
const baseHeight = iconBaseHeight * 0.3;
|
||||
const screenWidth = iconBaseWidth * 0.85;
|
||||
const screenX = nodeInfo.x - screenWidth / 2;
|
||||
const screenBezel = 3;
|
||||
|
||||
const linuxScreenClipId = `linux-screen-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
||||
defs
|
||||
.append("clipPath")
|
||||
.attr("id", linuxScreenClipId)
|
||||
.append("rect")
|
||||
.attr("x", screenX + screenBezel)
|
||||
.attr("y", y + screenBezel)
|
||||
.attr("width", screenWidth - screenBezel * 2)
|
||||
.attr("height", screenHeight - screenBezel * 2)
|
||||
.attr("rx", 2);
|
||||
|
||||
// Screen outer frame
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("class", "node-outline")
|
||||
.attr("x", screenX)
|
||||
.attr("y", y)
|
||||
.attr("width", screenWidth)
|
||||
.attr("height", screenHeight)
|
||||
.attr("rx", 3)
|
||||
.attr("fill", "#1a1a1a")
|
||||
.attr("stroke", wireColor)
|
||||
.attr("stroke-width", strokeWidth);
|
||||
|
||||
// Screen inner
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", screenX + screenBezel)
|
||||
.attr("y", y + screenBezel)
|
||||
.attr("width", screenWidth - screenBezel * 2)
|
||||
.attr("height", screenHeight - screenBezel * 2)
|
||||
.attr("rx", 2)
|
||||
.attr("fill", "#0a0a12");
|
||||
|
||||
// Memory fill on screen
|
||||
if (ramUsagePercent > 0) {
|
||||
const memFillTotalHeight = screenHeight - screenBezel * 2;
|
||||
const memFillActualHeight =
|
||||
(ramUsagePercent / 100) * memFillTotalHeight;
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", screenX + screenBezel)
|
||||
.attr(
|
||||
"y",
|
||||
y + screenBezel + (memFillTotalHeight - memFillActualHeight),
|
||||
)
|
||||
.attr("width", screenWidth - screenBezel * 2)
|
||||
.attr("height", memFillActualHeight)
|
||||
.attr("fill", "rgba(255,215,0,0.85)")
|
||||
.attr("clip-path", `url(#${linuxScreenClipId})`);
|
||||
}
|
||||
|
||||
// Terminal prompt on screen
|
||||
nodeG
|
||||
.append("text")
|
||||
.attr("x", nodeInfo.x)
|
||||
.attr("y", y + screenHeight / 2)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dominant-baseline", "middle")
|
||||
.attr("fill", "#FFFFFF")
|
||||
.attr("opacity", 0.9)
|
||||
.attr("font-size", screenHeight * 0.25)
|
||||
.attr("font-family", "SF Mono, Monaco, monospace")
|
||||
.attr("font-weight", "700")
|
||||
.text(">_");
|
||||
|
||||
// Keyboard base (trapezoidal)
|
||||
const baseY = y + screenHeight;
|
||||
const baseTopWidth = screenWidth;
|
||||
const baseBottomWidth = iconBaseWidth;
|
||||
const baseTopX = nodeInfo.x - baseTopWidth / 2;
|
||||
const baseBottomX = nodeInfo.x - baseBottomWidth / 2;
|
||||
|
||||
nodeG
|
||||
.append("path")
|
||||
.attr(
|
||||
"d",
|
||||
`M ${baseTopX} ${baseY} L ${baseTopX + baseTopWidth} ${baseY} L ${baseBottomX + baseBottomWidth} ${baseY + baseHeight} L ${baseBottomX} ${baseY + baseHeight} Z`,
|
||||
)
|
||||
.attr("fill", "#2c2c2c")
|
||||
.attr("stroke", wireColor)
|
||||
.attr("stroke-width", 1);
|
||||
|
||||
// Keyboard area
|
||||
const keyboardX = baseTopX + 6;
|
||||
const keyboardY = baseY + 3;
|
||||
const keyboardWidth = baseTopWidth - 12;
|
||||
const keyboardHeight = baseHeight * 0.55;
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", keyboardX)
|
||||
.attr("y", keyboardY)
|
||||
.attr("width", keyboardWidth)
|
||||
.attr("height", keyboardHeight)
|
||||
.attr("fill", "rgba(0,0,0,0.2)")
|
||||
.attr("rx", 2);
|
||||
|
||||
// Trackpad
|
||||
const trackpadWidth = baseTopWidth * 0.4;
|
||||
const trackpadX = nodeInfo.x - trackpadWidth / 2;
|
||||
const trackpadY = baseY + keyboardHeight + 5;
|
||||
const trackpadHeight = baseHeight * 0.3;
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", trackpadX)
|
||||
.attr("y", trackpadY)
|
||||
.attr("width", trackpadWidth)
|
||||
.attr("height", trackpadHeight)
|
||||
.attr("fill", "rgba(255,255,255,0.08)")
|
||||
.attr("rx", 2);
|
||||
} else if (isLinux) {
|
||||
// Linux Desktop — same shape as Mac Studio but with Tux logo
|
||||
iconBaseWidth = nodeRadius * 1.25;
|
||||
iconBaseHeight = nodeRadius * 0.85;
|
||||
const x = nodeInfo.x - iconBaseWidth / 2;
|
||||
const y = nodeInfo.y - iconBaseHeight / 2;
|
||||
const cornerRadius = 4;
|
||||
const topSurfaceHeight = iconBaseHeight * 0.15;
|
||||
|
||||
const linuxDesktopClipId = `linux-desktop-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
||||
defs
|
||||
.append("clipPath")
|
||||
.attr("id", linuxDesktopClipId)
|
||||
.append("rect")
|
||||
.attr("x", x)
|
||||
.attr("y", y + topSurfaceHeight)
|
||||
.attr("width", iconBaseWidth)
|
||||
.attr("height", iconBaseHeight - topSurfaceHeight)
|
||||
.attr("rx", cornerRadius - 1);
|
||||
|
||||
// Main body
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("class", "node-outline")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("width", iconBaseWidth)
|
||||
.attr("height", iconBaseHeight)
|
||||
.attr("rx", cornerRadius)
|
||||
.attr("fill", "#1a1a1a")
|
||||
.attr("stroke", wireColor)
|
||||
.attr("stroke-width", strokeWidth);
|
||||
|
||||
// Memory fill
|
||||
if (ramUsagePercent > 0) {
|
||||
const memFillTotalHeight = iconBaseHeight - topSurfaceHeight;
|
||||
const memFillActualHeight =
|
||||
(ramUsagePercent / 100) * memFillTotalHeight;
|
||||
nodeG
|
||||
.append("rect")
|
||||
.attr("x", x)
|
||||
.attr(
|
||||
"y",
|
||||
y + topSurfaceHeight + (memFillTotalHeight - memFillActualHeight),
|
||||
)
|
||||
.attr("width", iconBaseWidth)
|
||||
.attr("height", memFillActualHeight)
|
||||
.attr("fill", "rgba(255,215,0,0.75)")
|
||||
.attr("clip-path", `url(#${linuxDesktopClipId})`);
|
||||
}
|
||||
|
||||
// Terminal prompt on front face
|
||||
nodeG
|
||||
.append("text")
|
||||
.attr("x", nodeInfo.x)
|
||||
.attr(
|
||||
"y",
|
||||
y + topSurfaceHeight + (iconBaseHeight - topSurfaceHeight) / 2,
|
||||
)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dominant-baseline", "middle")
|
||||
.attr("fill", "rgba(255,255,255,0.5)")
|
||||
.attr("font-size", (iconBaseHeight - topSurfaceHeight) * 0.4)
|
||||
.attr("font-family", "SF Mono, Monaco, monospace")
|
||||
.attr("font-weight", "700")
|
||||
.text(">_");
|
||||
} else if (modelLower === "mac studio") {
|
||||
// Mac Studio - classic cube with memory fill
|
||||
iconBaseWidth = nodeRadius * 1.25;
|
||||
iconBaseHeight = nodeRadius * 0.85;
|
||||
@@ -1182,8 +1568,12 @@
|
||||
debugLabelY += debugLineHeight;
|
||||
}
|
||||
|
||||
const identity = identitiesData[nodeInfo.id];
|
||||
if (identity?.osVersion) {
|
||||
const dbgIdentity = identitiesData[nodeInfo.id];
|
||||
if (dbgIdentity?.osVersion) {
|
||||
const osLabel =
|
||||
dbgIdentity.osVersion === "Linux"
|
||||
? "Linux"
|
||||
: `macOS ${dbgIdentity.osVersion}${dbgIdentity.osBuildVersion ? ` (${dbgIdentity.osBuildVersion})` : ""}`;
|
||||
nodeG
|
||||
.append("text")
|
||||
.attr("x", nodeInfo.x)
|
||||
@@ -1192,9 +1582,7 @@
|
||||
.attr("fill", "rgba(179,179,179,0.7)")
|
||||
.attr("font-size", debugFontSize)
|
||||
.attr("font-family", "SF Mono, Monaco, monospace")
|
||||
.text(
|
||||
`macOS ${identity.osVersion}${identity.osBuildVersion ? ` (${identity.osBuildVersion})` : ""}`,
|
||||
);
|
||||
.text(osLabel);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/DeepSeek-V3.1-GGUF"
|
||||
n_layers = 61
|
||||
hidden_size = 7168
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "deepseek"
|
||||
quantization = "GGUF"
|
||||
base_model = "DeepSeek V3.1"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 684531386000
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/GLM-4.7-Flash-GGUF"
|
||||
n_layers = 47
|
||||
hidden_size = 2048
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "glm"
|
||||
quantization = "GGUF"
|
||||
base_model = "GLM 4.7 Flash"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 35624923488
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/GLM-5-GGUF"
|
||||
n_layers = 78
|
||||
hidden_size = 6144
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "glm"
|
||||
quantization = "GGUF"
|
||||
base_model = "GLM-5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 1487822475264
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Llama-3.2-1B-Instruct-GGUF"
|
||||
n_layers = 16
|
||||
hidden_size = 2048
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "llama"
|
||||
quantization = "GGUF"
|
||||
base_model = "Llama 3.2 1B"
|
||||
capabilities = ["text"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 2479595264
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Llama-3.2-3B-Instruct-GGUF"
|
||||
n_layers = 28
|
||||
hidden_size = 3072
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "llama"
|
||||
quantization = "GGUF"
|
||||
base_model = "Llama 3.2 3B"
|
||||
capabilities = ["text"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 6433687744
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Llama-3.3-70B-Instruct-GGUF"
|
||||
n_layers = 80
|
||||
hidden_size = 8192
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "llama"
|
||||
quantization = "GGUF"
|
||||
base_model = "Llama 3.3 70B"
|
||||
capabilities = ["text"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 144383672320
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/MiniMax-M2.5-GGUF"
|
||||
n_layers = 62
|
||||
hidden_size = 3072
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "minimax"
|
||||
quantization = "GGUF"
|
||||
base_model = "MiniMax M2.5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 228703644928
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Qwen3-0.6B-GGUF"
|
||||
n_layers = 28
|
||||
hidden_size = 1024
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "qwen"
|
||||
quantization = "GGUF"
|
||||
base_model = "Qwen3 0.6B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 1198182848
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Qwen3-30B-A3B-GGUF"
|
||||
n_layers = 48
|
||||
hidden_size = 2048
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "qwen"
|
||||
quantization = "GGUF"
|
||||
base_model = "Qwen3 30B A3B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 30532122624
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Qwen3-Coder-Next-GGUF"
|
||||
n_layers = 48
|
||||
hidden_size = 2048
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "qwen"
|
||||
quantization = "GGUF"
|
||||
base_model = "Qwen3 Coder Next"
|
||||
capabilities = ["text", "code"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 157548627945
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Qwen3.5-122B-A10B-GGUF"
|
||||
n_layers = 48
|
||||
hidden_size = 3072
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "qwen"
|
||||
quantization = "GGUF"
|
||||
base_model = "Qwen3.5 122B A10B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 245125640160
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Qwen3.5-27B-GGUF"
|
||||
n_layers = 64
|
||||
hidden_size = 5120
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "qwen"
|
||||
quantization = "GGUF"
|
||||
base_model = "Qwen3.5 27B"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 27781427952
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Qwen3.5-2B-GGUF"
|
||||
n_layers = 24
|
||||
hidden_size = 2048
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "qwen"
|
||||
quantization = "GGUF"
|
||||
base_model = "Qwen3.5 2B"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 3775709216
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Qwen3.5-35B-A3B-GGUF"
|
||||
n_layers = 40
|
||||
hidden_size = 2048
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "qwen"
|
||||
quantization = "GGUF"
|
||||
base_model = "Qwen3.5 35B A3B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 35951822704
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/Qwen3.5-9B-GGUF"
|
||||
n_layers = 32
|
||||
hidden_size = 4096
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "qwen"
|
||||
quantization = "GGUF"
|
||||
base_model = "Qwen3.5 9B"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 17920697312
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/gpt-oss-120b-GGUF"
|
||||
n_layers = 36
|
||||
hidden_size = 2880
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "gpt-oss"
|
||||
quantization = "GGUF"
|
||||
base_model = "GPT-OSS 120B"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 65369017728
|
||||
@@ -0,0 +1,12 @@
|
||||
model_id = "unsloth/gpt-oss-20b-GGUF"
|
||||
n_layers = 24
|
||||
hidden_size = 2880
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "gpt-oss"
|
||||
quantization = "GGUF"
|
||||
base_model = "GPT-OSS 20B"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 13792639168
|
||||
@@ -0,0 +1,265 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import mlx.core as mx
|
||||
from loguru import logger
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Architecture mapping: GGUF general.architecture → HuggingFace
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ARCH_TO_HF: dict[str, list[str]] = {
|
||||
"llama": ["LlamaForCausalLM"],
|
||||
"qwen2": ["Qwen2ForCausalLM"],
|
||||
"qwen3": ["Qwen3ForCausalLM"],
|
||||
"mistral": ["MistralForCausalLM"],
|
||||
"phi3": ["Phi3ForCausalLM"],
|
||||
"gemma": ["GemmaForCausalLM"],
|
||||
"gemma2": ["Gemma2ForCausalLM"],
|
||||
"starcoder2": ["Starcoder2ForCausalLM"],
|
||||
}
|
||||
|
||||
_ARCH_TO_MODEL_TYPE: dict[str, str] = {
|
||||
"llama": "llama",
|
||||
"qwen2": "qwen2",
|
||||
"qwen3": "qwen3",
|
||||
"mistral": "mistral",
|
||||
"phi3": "phi3",
|
||||
"gemma": "gemma",
|
||||
"gemma2": "gemma2",
|
||||
"starcoder2": "starcoder2",
|
||||
}
|
||||
|
||||
# GGUF general.file_type → quantization config for mlx_lm
|
||||
_FILE_TYPE_TO_QUANT: dict[int, dict[str, int] | None] = {
|
||||
0: None, # ALL_F32
|
||||
1: None, # MOSTLY_F16
|
||||
2: {"group_size": 32, "bits": 4}, # MOSTLY_Q4_0
|
||||
3: {"group_size": 32, "bits": 4}, # MOSTLY_Q4_1
|
||||
7: {"group_size": 32, "bits": 8}, # MOSTLY_Q8_0
|
||||
}
|
||||
|
||||
# Maximum safetensors shard size in bytes (5 GB)
|
||||
_MAX_SHARD_BYTES = 5 * 1024 * 1024 * 1024
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Weight name translation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def translate_gguf_weight_name(name: str) -> str:
|
||||
"""Translate a GGUF tensor name to HuggingFace-style name.
|
||||
|
||||
Vendored from: https://github.com/ml-explore/mlx-examples/blob/main/llms/gguf_llm/models.py
|
||||
(translate_weight_names function)
|
||||
|
||||
Covers the standard Llama-family convention which is shared by
|
||||
Llama, Qwen, Mistral, Gemma, Phi, and most other transformer models.
|
||||
"""
|
||||
name = name.replace("blk.", "model.layers.")
|
||||
name = name.replace("ffn_gate", "mlp.gate_proj")
|
||||
name = name.replace("ffn_down", "mlp.down_proj")
|
||||
name = name.replace("ffn_up", "mlp.up_proj")
|
||||
name = name.replace("attn_q", "self_attn.q_proj")
|
||||
name = name.replace("attn_k", "self_attn.k_proj")
|
||||
name = name.replace("attn_v", "self_attn.v_proj")
|
||||
name = name.replace("attn_output", "self_attn.o_proj")
|
||||
name = name.replace("attn_norm", "input_layernorm")
|
||||
name = name.replace("ffn_norm", "post_attention_layernorm")
|
||||
name = name.replace("token_embd", "model.embed_tokens")
|
||||
name = name.replace("output_norm", "model.norm")
|
||||
# "output" must come last — it's a substring of "attn_output" / "output_norm"
|
||||
if name == "output.weight":
|
||||
name = "lm_head.weight"
|
||||
return name
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Metadata extraction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _meta_val(metadata: dict[str, Any], key: str) -> int | float | str:
|
||||
"""Get a metadata value, converting mx.array scalars to Python types."""
|
||||
val = metadata[key] # pyright: ignore[reportAny]
|
||||
if isinstance(val, mx.array):
|
||||
return int(val.item()) # pyright: ignore[reportAny]
|
||||
if isinstance(val, (int, float)):
|
||||
return val
|
||||
return str(val) # pyright: ignore[reportAny]
|
||||
|
||||
|
||||
def extract_config_from_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Build a HuggingFace-style config.json dict from GGUF metadata."""
|
||||
raw_arch: str = str(metadata.get("general.architecture", "llama")) # pyright: ignore[reportAny]
|
||||
arch: str = raw_arch.lower()
|
||||
prefix = f"{arch}."
|
||||
|
||||
def get(suffix: str, default: int | float | str | None = None) -> int | float | str | None:
|
||||
key = prefix + suffix
|
||||
if key not in metadata:
|
||||
return default
|
||||
return _meta_val(metadata, key)
|
||||
|
||||
# Vocab size: prefer metadata field, fall back to tokenizer token count
|
||||
vocab_size: int | float | str | None = get("vocab_size")
|
||||
if vocab_size is None:
|
||||
tokens: Any = metadata.get("tokenizer.ggml.tokens")
|
||||
vocab_size = len(tokens) if tokens is not None else 0 # pyright: ignore[reportAny]
|
||||
|
||||
config: dict[str, Any] = {
|
||||
"architectures": _ARCH_TO_HF.get(arch, ["LlamaForCausalLM"]),
|
||||
"model_type": _ARCH_TO_MODEL_TYPE.get(arch, arch),
|
||||
"hidden_size": get("embedding_length", 0),
|
||||
"num_hidden_layers": get("block_count", 0),
|
||||
"num_attention_heads": get("attention.head_count", 0),
|
||||
"num_key_value_heads": get("attention.head_count_kv"),
|
||||
"intermediate_size": get("feed_forward_length", 0),
|
||||
"vocab_size": vocab_size,
|
||||
"rms_norm_eps": get("attention.layer_norm_rms_epsilon", 1e-5),
|
||||
"rope_theta": get("rope.freq_base", 10000.0),
|
||||
"max_position_embeddings": get("context_length", 4096),
|
||||
"tie_word_embeddings": False,
|
||||
}
|
||||
|
||||
# Remove None values
|
||||
config = {k: v for k, v in config.items() if v is not None} # pyright: ignore[reportAny]
|
||||
|
||||
# Quantization config
|
||||
file_type_raw: Any = metadata.get("general.file_type")
|
||||
if file_type_raw is not None:
|
||||
file_type = int(_meta_val(metadata, "general.file_type"))
|
||||
quant = _FILE_TYPE_TO_QUANT.get(file_type)
|
||||
if quant is not None:
|
||||
config["quantization_config"] = quant
|
||||
|
||||
return config
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Conversion
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _load_gguf(gguf_path: Path) -> tuple[dict[str, mx.array], dict[str, Any]]:
|
||||
"""Load a GGUF file, returning (weights, metadata)."""
|
||||
result: Any = mx.load(str(gguf_path), return_metadata=True) # pyright: ignore[reportAny]
|
||||
weights: dict[str, mx.array] = result[0] # pyright: ignore[reportAny]
|
||||
metadata: dict[str, Any] = result[1] # pyright: ignore[reportAny]
|
||||
return weights, metadata
|
||||
|
||||
|
||||
def convert_gguf_to_safetensors(gguf_path: Path, output_dir: Path) -> None:
|
||||
"""Convert a GGUF file to safetensors + config.json in output_dir.
|
||||
|
||||
After conversion:
|
||||
- output_dir contains model*.safetensors, config.json,
|
||||
model.safetensors.index.json, and a .gguf_converted marker
|
||||
- The original .gguf file is deleted to save disk space
|
||||
"""
|
||||
logger.info(f"Loading GGUF file: {gguf_path}")
|
||||
weights, metadata = _load_gguf(gguf_path)
|
||||
|
||||
# Translate weight names
|
||||
translated: dict[str, mx.array] = {}
|
||||
for name, tensor in weights.items():
|
||||
new_name = translate_gguf_weight_name(name)
|
||||
translated[new_name] = tensor
|
||||
del weights # Free memory
|
||||
|
||||
# Extract and write config.json
|
||||
config = extract_config_from_metadata(metadata)
|
||||
config_path = output_dir / "config.json"
|
||||
if not config_path.exists():
|
||||
logger.info(f"Writing config.json to {config_path}")
|
||||
config_path.write_text(json.dumps(config, indent=2, default=str))
|
||||
|
||||
# Shard weights and save as safetensors
|
||||
weight_map: dict[str, str] = {}
|
||||
shards: list[tuple[str, dict[str, mx.array]]] = []
|
||||
current_shard: dict[str, mx.array] = {}
|
||||
current_size = 0
|
||||
shard_idx = 0
|
||||
|
||||
for name, tensor in sorted(translated.items()):
|
||||
tensor_bytes = tensor.nbytes
|
||||
if current_size + tensor_bytes > _MAX_SHARD_BYTES and current_shard:
|
||||
shard_idx += 1
|
||||
shards.append((f"model-{shard_idx:05d}", current_shard))
|
||||
current_shard = {}
|
||||
current_size = 0
|
||||
current_shard[name] = tensor
|
||||
current_size += tensor_bytes
|
||||
|
||||
if current_shard:
|
||||
shard_idx += 1
|
||||
shards.append((f"model-{shard_idx:05d}", current_shard))
|
||||
|
||||
total_shards = len(shards)
|
||||
total_size = 0
|
||||
|
||||
for shard_name, shard_weights in shards:
|
||||
if total_shards == 1:
|
||||
filename = "model.safetensors"
|
||||
else:
|
||||
filename = f"{shard_name}-of-{total_shards:05d}.safetensors"
|
||||
|
||||
shard_path = output_dir / filename
|
||||
logger.info(f"Writing {filename} ({len(shard_weights)} tensors)")
|
||||
mx.save_safetensors(str(shard_path), shard_weights) # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
for weight_name in shard_weights:
|
||||
weight_map[weight_name] = filename
|
||||
total_size += shard_weights[weight_name].nbytes
|
||||
|
||||
del translated # Free memory
|
||||
|
||||
# Write safetensors index
|
||||
index = {
|
||||
"metadata": {"total_size": total_size},
|
||||
"weight_map": weight_map,
|
||||
}
|
||||
index_path = output_dir / "model.safetensors.index.json"
|
||||
logger.info(f"Writing safetensors index to {index_path}")
|
||||
index_path.write_text(json.dumps(index, indent=2))
|
||||
|
||||
# Write marker
|
||||
marker_path = output_dir / ".gguf_converted"
|
||||
marker_path.write_text(f"Converted from {gguf_path.name}\n")
|
||||
|
||||
# Delete original GGUF file
|
||||
logger.info(f"Deleting original GGUF file: {gguf_path}")
|
||||
gguf_path.unlink()
|
||||
|
||||
logger.info("GGUF → safetensors conversion complete")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers for model card creation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def is_gguf_repo(file_paths: list[str]) -> bool:
|
||||
"""Check if a file list represents a GGUF-only repo (has .gguf but no .safetensors)."""
|
||||
has_gguf = any(p.endswith(".gguf") for p in file_paths)
|
||||
has_safetensors = any(p.endswith(".safetensors") for p in file_paths)
|
||||
return has_gguf and not has_safetensors
|
||||
|
||||
|
||||
def select_gguf_file(file_paths: list[str], file_sizes: dict[str, int | None] | None = None) -> str | None:
|
||||
"""Pick the best GGUF file from a file list.
|
||||
|
||||
Prefers the largest file (usually highest quality quant).
|
||||
"""
|
||||
gguf_files = [p for p in file_paths if p.endswith(".gguf")]
|
||||
if not gguf_files:
|
||||
return None
|
||||
|
||||
if file_sizes:
|
||||
# Pick largest
|
||||
return max(gguf_files, key=lambda p: file_sizes.get(p, 0) or 0)
|
||||
|
||||
# No size info — just pick first
|
||||
return gguf_files[0]
|
||||
@@ -115,6 +115,19 @@ class ResumableShardDownloader(ShardDownloader):
|
||||
allow_patterns=allow_patterns,
|
||||
skip_internet=self.offline,
|
||||
)
|
||||
|
||||
if not config_only and target_dir.suffix == ".gguf":
|
||||
output_dir = target_dir.parent
|
||||
marker = output_dir / ".gguf_converted"
|
||||
if not marker.exists():
|
||||
from exo.download.gguf_conversion import convert_gguf_to_safetensors
|
||||
|
||||
logger.info(f"Converting GGUF to safetensors: {target_dir}")
|
||||
await asyncio.to_thread(
|
||||
convert_gguf_to_safetensors, target_dir, output_dir
|
||||
)
|
||||
target_dir = output_dir
|
||||
|
||||
return target_dir
|
||||
|
||||
async def get_shard_download_status(
|
||||
|
||||
@@ -126,10 +126,12 @@ class ModelCard(CamelCaseModel):
|
||||
@staticmethod
|
||||
async def fetch_from_hf(model_id: ModelId) -> "ModelCard":
|
||||
"""Fetches storage size and number of layers for a Hugging Face model, returns Pydantic ModelMeta."""
|
||||
# TODO: failure if files do not exist
|
||||
config_data = await fetch_config_data(model_id)
|
||||
try:
|
||||
config_data = await fetch_config_data(model_id)
|
||||
except FileNotFoundError:
|
||||
config_data = await fetch_config_data_for_gguf(model_id)
|
||||
num_layers = config_data.layer_count
|
||||
mem_size_bytes = await fetch_safetensors_size(model_id)
|
||||
mem_size_bytes = await fetch_model_size(model_id)
|
||||
|
||||
mc = ModelCard(
|
||||
model_id=ModelId(model_id),
|
||||
@@ -244,8 +246,38 @@ async def fetch_config_data(model_id: ModelId) -> ConfigData:
|
||||
return ConfigData.model_validate_json(await f.read())
|
||||
|
||||
|
||||
async def fetch_safetensors_size(model_id: ModelId) -> Memory:
|
||||
"""Gets model size from safetensors index or falls back to HF API."""
|
||||
async def fetch_config_data_for_gguf(model_id: ModelId) -> ConfigData:
|
||||
# Try to get the base model from HF API
|
||||
info = model_info(model_id)
|
||||
base_model_id: str | None = None
|
||||
|
||||
# card_data has base_model field
|
||||
if info.card_data is not None:
|
||||
base_model_raw: str | list[str] | None = getattr(info.card_data, "base_model", None) # pyright: ignore[reportAttributeAccessIssue, reportAny]
|
||||
if isinstance(base_model_raw, str):
|
||||
base_model_id = base_model_raw
|
||||
elif isinstance(base_model_raw, list) and base_model_raw:
|
||||
# Pick the first non-quantized base model
|
||||
for bm in base_model_raw:
|
||||
if isinstance(bm, str) and "quantized:" not in bm:
|
||||
base_model_id = bm
|
||||
break
|
||||
|
||||
if base_model_id is not None:
|
||||
logger.info(f"GGUF repo {model_id} references base_model={base_model_id}, fetching its config")
|
||||
try:
|
||||
return await fetch_config_data(ModelId(base_model_id))
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to fetch config from base model {base_model_id}: {exc}")
|
||||
|
||||
raise ValueError(
|
||||
f"Model {model_id} has no config.json and no base_model reference. "
|
||||
f"Cannot determine model architecture."
|
||||
)
|
||||
|
||||
|
||||
async def fetch_model_size(model_id: ModelId) -> Memory:
|
||||
"""Gets model size from safetensors index, HF API, or GGUF file sizes."""
|
||||
from exo.download.download_utils import (
|
||||
download_file_with_retry,
|
||||
ensure_models_dir,
|
||||
@@ -254,23 +286,36 @@ async def fetch_safetensors_size(model_id: ModelId) -> Memory:
|
||||
|
||||
target_dir = (await ensure_models_dir()) / model_id.normalize()
|
||||
await aios.makedirs(target_dir, exist_ok=True)
|
||||
index_path = await download_file_with_retry(
|
||||
model_id,
|
||||
"main",
|
||||
"model.safetensors.index.json",
|
||||
target_dir,
|
||||
lambda curr_bytes, total_bytes, is_renamed: logger.debug(
|
||||
f"Downloading model.safetensors.index.json for {model_id}: {curr_bytes}/{total_bytes} ({is_renamed=})"
|
||||
),
|
||||
)
|
||||
async with aiofiles.open(index_path, "r") as f:
|
||||
index_data = ModelSafetensorsIndex.model_validate_json(await f.read())
|
||||
try:
|
||||
index_path = await download_file_with_retry(
|
||||
model_id,
|
||||
"main",
|
||||
"model.safetensors.index.json",
|
||||
target_dir,
|
||||
lambda curr_bytes, total_bytes, is_renamed: logger.debug(
|
||||
f"Downloading model.safetensors.index.json for {model_id}: {curr_bytes}/{total_bytes} ({is_renamed=})"
|
||||
),
|
||||
)
|
||||
async with aiofiles.open(index_path, "r") as f:
|
||||
index_data = ModelSafetensorsIndex.model_validate_json(await f.read())
|
||||
|
||||
metadata = index_data.metadata
|
||||
if metadata is not None:
|
||||
return Memory.from_bytes(metadata.total_size)
|
||||
metadata = index_data.metadata
|
||||
if metadata is not None:
|
||||
return Memory.from_bytes(metadata.total_size)
|
||||
except FileNotFoundError:
|
||||
logger.debug(f"No safetensors index for {model_id}, falling back to HF API")
|
||||
|
||||
info = model_info(model_id)
|
||||
if info.safetensors is None:
|
||||
raise ValueError(f"No safetensors info found for {model_id}")
|
||||
return Memory.from_bytes(info.safetensors.total)
|
||||
if info.safetensors is not None:
|
||||
return Memory.from_bytes(info.safetensors.total)
|
||||
|
||||
# GGUF fallback: sum .gguf file sizes
|
||||
gguf_size = sum(
|
||||
s.size
|
||||
for s in (info.siblings or [])
|
||||
if s.rfilename.endswith(".gguf") and s.size is not None
|
||||
)
|
||||
if gguf_size > 0:
|
||||
return Memory.from_bytes(gguf_size)
|
||||
|
||||
raise ValueError(f"No safetensors or GGUF files found for {model_id}")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import platform
|
||||
import socket
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
import psutil
|
||||
@@ -117,12 +118,97 @@ async def get_network_interfaces() -> list[NetworkInterfaceInfo]:
|
||||
return interfaces_info
|
||||
|
||||
|
||||
def _read_dmi_field(name: str) -> str | None:
|
||||
"""Read a single DMI sysfs field, returning ``None`` on failure."""
|
||||
try:
|
||||
path = Path(f"/sys/class/dmi/id/{name}")
|
||||
if path.exists():
|
||||
return path.read_text().strip()
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
async def _get_linux_model_and_chip() -> tuple[str, str]:
|
||||
"""Get Linux system information using DMI and /proc/cpuinfo.
|
||||
|
||||
Detects NVIDIA DGX Spark (DMI product_name ``"DGX_Spark"``) and other
|
||||
NVIDIA systems via the ``sys_vendor`` DMI field, falling back to generic
|
||||
Linux identification.
|
||||
"""
|
||||
model = "Linux"
|
||||
chip = "Unknown Chip"
|
||||
|
||||
product_name = _read_dmi_field("product_name")
|
||||
sys_vendor = _read_dmi_field("sys_vendor")
|
||||
|
||||
# DGX Spark: DMI product_name may be "DGX_Spark" or "gx10" variant
|
||||
product_lower = (product_name or "").lower()
|
||||
if product_name and ("dgx" in product_lower or "gx10" in product_lower):
|
||||
model = "DGX Spark"
|
||||
try:
|
||||
process = await run_process(
|
||||
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"]
|
||||
)
|
||||
gpu_name = process.stdout.decode().strip().split("\n")[0]
|
||||
chip = gpu_name if gpu_name and gpu_name != "[N/A]" else "NVIDIA GB10"
|
||||
except (CalledProcessError, FileNotFoundError):
|
||||
chip = "NVIDIA GB10"
|
||||
return (model, chip)
|
||||
|
||||
# Other NVIDIA systems (sys_vendor contains "NVIDIA")
|
||||
if sys_vendor and "NVIDIA" in sys_vendor:
|
||||
model = product_name.replace("_", " ") if product_name else "NVIDIA System"
|
||||
try:
|
||||
process = await run_process(
|
||||
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"]
|
||||
)
|
||||
gpu_name = process.stdout.decode().strip().split("\n")[0]
|
||||
if gpu_name and gpu_name != "[N/A]":
|
||||
chip = gpu_name
|
||||
except (CalledProcessError, FileNotFoundError):
|
||||
pass
|
||||
return (model, chip)
|
||||
|
||||
# Generic Linux — detect laptop vs desktop via chassis_type
|
||||
# SMBIOS chassis types: 8,9,10,14,31,32 = portable/laptop
|
||||
chassis_type = _read_dmi_field("chassis_type")
|
||||
laptop_chassis_types = {"8", "9", "10", "14", "31", "32"}
|
||||
if chassis_type in laptop_chassis_types:
|
||||
model = "Linux Laptop"
|
||||
elif chassis_type is not None:
|
||||
model = "Linux Desktop"
|
||||
|
||||
# Also check for battery as a fallback laptop indicator
|
||||
if model == "Linux" and Path("/sys/class/power_supply/BAT0").exists():
|
||||
model = "Linux Laptop"
|
||||
|
||||
# Use /proc/cpuinfo for chip
|
||||
cpuinfo_path = Path("/proc/cpuinfo")
|
||||
if cpuinfo_path.exists():
|
||||
try:
|
||||
for line in cpuinfo_path.read_text().splitlines():
|
||||
if line.startswith("model name"):
|
||||
chip = line.split(":", 1)[1].strip()
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return (model, chip)
|
||||
|
||||
|
||||
async def get_model_and_chip() -> tuple[str, str]:
|
||||
"""Get Mac system information using system_profiler."""
|
||||
"""Get system model and chip information.
|
||||
|
||||
On macOS, uses ``system_profiler``. On Linux, reads DMI data from
|
||||
sysfs and CPU info from ``/proc/cpuinfo``.
|
||||
"""
|
||||
model = "Unknown Model"
|
||||
chip = "Unknown Chip"
|
||||
|
||||
# TODO: better non mac support
|
||||
if sys.platform == "linux":
|
||||
return await _get_linux_model_and_chip()
|
||||
|
||||
if sys.platform != "darwin":
|
||||
return (model, chip)
|
||||
|
||||
|
||||
@@ -568,14 +568,21 @@ def apply_chat_template(
|
||||
"Patched lossy chat template (removed inner_type length guard)"
|
||||
)
|
||||
|
||||
prompt: str = tokenizer.apply_chat_template(
|
||||
formatted_messages,
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
tools=task_params.tools,
|
||||
**({"chat_template": patched_template} if patched_template is not None else {}),
|
||||
**extra_kwargs,
|
||||
)
|
||||
try:
|
||||
prompt: str = tokenizer.apply_chat_template(
|
||||
formatted_messages,
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
tools=task_params.tools,
|
||||
**({"chat_template": patched_template} if patched_template is not None else {}),
|
||||
**extra_kwargs,
|
||||
)
|
||||
except ValueError:
|
||||
logger.warning("No chat template found, falling back to raw message concatenation")
|
||||
prompt = "\n".join(
|
||||
f"{msg.get('role', 'user')}: {msg.get('content', '')}"
|
||||
for msg in formatted_messages
|
||||
)
|
||||
|
||||
if task_params.tools and _schemas_lost_in_prompt(prompt, task_params.tools):
|
||||
logger.warning("Chat template lost nested tool schemas even after patching")
|
||||
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
msg() {
|
||||
printf '\n==> %s\n' "$*"
|
||||
}
|
||||
|
||||
msg "Bringing down old Apple USB fallback interfaces"
|
||||
sudo ifconfig en4 down 2>/dev/null || true
|
||||
sudo ifconfig en5 down 2>/dev/null || true
|
||||
sudo ifconfig en6 down 2>/dev/null || true
|
||||
|
||||
msg "Bringing down current anpi interfaces"
|
||||
sudo ifconfig anpi0 down 2>/dev/null || true
|
||||
sudo ifconfig anpi1 down 2>/dev/null || true
|
||||
sudo ifconfig anpi2 down 2>/dev/null || true
|
||||
|
||||
msg "Stopping likely bad exporter services"
|
||||
sudo launchctl bootout system/com.apple.usbmuxd 2>/dev/null || true
|
||||
sudo launchctl bootout system/com.apple.remoted 2>/dev/null || true
|
||||
launchctl bootout "gui/$(id -u)/com.apple.remoted" 2>/dev/null || true
|
||||
|
||||
sudo pkill -x usbmuxd 2>/dev/null || true
|
||||
sudo pkill -x remoted 2>/dev/null || true
|
||||
|
||||
cat <<'EOF'
|
||||
|
||||
Now unplug and replug the cable.
|
||||
|
||||
After reconnect, press Enter to continue.
|
||||
EOF
|
||||
|
||||
read -r _
|
||||
|
||||
msg "Bringing up anpi interfaces and requesting DHCP"
|
||||
for ifn in anpi0 anpi1 anpi2; do
|
||||
sudo ifconfig "$ifn" up 2>/dev/null || true
|
||||
sudo ipconfig set "$ifn" DHCP 2>/dev/null || true
|
||||
done
|
||||
|
||||
sleep 3
|
||||
|
||||
msg "Resulting interface state"
|
||||
for ifn in anpi0 anpi1 anpi2; do
|
||||
echo
|
||||
echo "--- $ifn ---"
|
||||
ifconfig "$ifn" 2>/dev/null || true
|
||||
ipconfig getifaddr "$ifn" 2>/dev/null || true
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Use whichever interface got 10.42.0.x or 10.43.0.x"
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
KVER="$(uname -r)"
|
||||
WORKDIR="${HOME}/apple1905-cdcncm"
|
||||
SRCVER="6.14.0-1015.15"
|
||||
SRCNAME="linux-nvidia-6.14"
|
||||
BASE_URL="https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/${SRCNAME}/${SRCVER}"
|
||||
|
||||
ORIG="${SRCNAME}_6.14.0.orig.tar.gz"
|
||||
DIFF="${SRCNAME}_${SRCVER}.diff.gz"
|
||||
DSC="${SRCNAME}_${SRCVER}.dsc"
|
||||
|
||||
KBUILD="/lib/modules/${KVER}/build"
|
||||
|
||||
msg() { printf '\n==> %s\n' "$*"; }
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
need_cmd() { command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"; }
|
||||
|
||||
[[ $EUID -eq 0 ]] || die "Run with sudo"
|
||||
[[ -d $KBUILD ]] || die "Missing kernel headers/build dir: $KBUILD"
|
||||
|
||||
need_cmd curl
|
||||
need_cmd dpkg-source
|
||||
need_cmd make
|
||||
need_cmd python3
|
||||
need_cmd modprobe
|
||||
need_cmd insmod
|
||||
need_cmd modinfo
|
||||
need_cmd ip
|
||||
need_cmd dmesg
|
||||
need_cmd find
|
||||
|
||||
msg "Installing build deps"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
linux-headers-"${KVER}" \
|
||||
dpkg-dev \
|
||||
ca-certificates \
|
||||
curl \
|
||||
kmod \
|
||||
zstd \
|
||||
python3
|
||||
|
||||
msg "Preparing workspace"
|
||||
rm -rf "$WORKDIR"
|
||||
mkdir -p "$WORKDIR"
|
||||
cd "$WORKDIR"
|
||||
|
||||
msg "Downloading exact source package for ${SRCNAME} ${SRCVER}"
|
||||
curl -fL "${BASE_URL}/${ORIG}" -o "${ORIG}"
|
||||
curl -fL "${BASE_URL}/${DIFF}" -o "${DIFF}"
|
||||
curl -fL "${BASE_URL}/${DSC}" -o "${DSC}"
|
||||
|
||||
msg "Extracting source"
|
||||
dpkg-source -x "${DSC}"
|
||||
|
||||
SRCDIR="$(find . -maxdepth 1 -mindepth 1 -type d -name "${SRCNAME}-*" | head -n1)"
|
||||
[[ -n ${SRCDIR} ]] || die "Could not find extracted source directory"
|
||||
cd "${SRCDIR}"
|
||||
|
||||
[[ -f drivers/net/usb/cdc_ncm.c ]] || die "cdc_ncm.c not found in source tree"
|
||||
|
||||
msg "Patching cdc_ncm.c"
|
||||
python3 <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
p = Path("drivers/net/usb/cdc_ncm.c")
|
||||
s = p.read_text()
|
||||
|
||||
if "0x05ac, 0x1905, 0" not in s:
|
||||
block = (
|
||||
'\t/* Apple Mac direct USB-C networking quirk */\n'
|
||||
'\t{ USB_DEVICE_INTERFACE_NUMBER(0x05ac, 0x1905, 0),\n'
|
||||
'\t .driver_info = (unsigned long)&apple_private_interface_info,\n'
|
||||
'\t},\n'
|
||||
'\t{ USB_DEVICE_INTERFACE_NUMBER(0x05ac, 0x1905, 2),\n'
|
||||
'\t .driver_info = (unsigned long)&apple_private_interface_info,\n'
|
||||
'\t},\n'
|
||||
)
|
||||
|
||||
pat = re.compile(
|
||||
r'(\{\s*USB_INTERFACE_INFO\s*\(\s*USB_CLASS_COMM\s*,\s*USB_CDC_SUBCLASS_NCM\s*,\s*USB_CDC_PROTO_NONE\s*\)\s*,\s*\.driver_info\s*=\s*\(unsigned long\)&cdc_ncm_info\s*,\s*\},)',
|
||||
re.S,
|
||||
)
|
||||
s2, n = pat.subn(block + r'\1', s, count=1)
|
||||
if n != 1:
|
||||
raise SystemExit("Could not find generic CDC NCM class-match entry to patch")
|
||||
s = s2
|
||||
|
||||
# Patch old bind check if needed.
|
||||
# Old style:
|
||||
# if (!dev->in || !dev->out || !dev->status)
|
||||
# New behavior for this Mac:
|
||||
# allow missing status endpoint for 05ac:1905
|
||||
old = re.compile(
|
||||
r'if\s*\(\s*!dev->in\s*\|\|\s*!dev->out\s*\|\|\s*!dev->status\s*\)',
|
||||
re.S,
|
||||
)
|
||||
repl = (
|
||||
'if (!dev->in || !dev->out || '
|
||||
'(!dev->status && '
|
||||
'!(le16_to_cpu(dev->udev->descriptor.idVendor) == 0x05ac && '
|
||||
'le16_to_cpu(dev->udev->descriptor.idProduct) == 0x1905)))'
|
||||
)
|
||||
s, n = old.subn(repl, s, count=1)
|
||||
|
||||
# If the tree already has the newer FLAG_LINK_INTR logic, leave it alone.
|
||||
if n == 0 and "FLAG_LINK_INTR" not in s:
|
||||
raise SystemExit("Could not patch bind_common() missing-status check")
|
||||
|
||||
# Add a loud marker so we know the patched module actually loaded.
|
||||
if 'Apple 05ac:1905 quirk test module loaded' not in s:
|
||||
m = re.search(r'static\s+int\s+__init\s+cdc_ncm_init\s*\(\s*void\s*\)\s*\{', s)
|
||||
if m:
|
||||
insert_at = m.end()
|
||||
s = s[:insert_at] + '\n\tpr_info("cdc_ncm: Apple 05ac:1905 quirk test module loaded\\n");' + s[insert_at:]
|
||||
|
||||
p.write_text(s)
|
||||
print("Patched", p)
|
||||
PY
|
||||
|
||||
msg "Preparing out-of-tree build dir"
|
||||
mkdir -p buildmod
|
||||
cp drivers/net/usb/cdc_ncm.c buildmod/
|
||||
|
||||
cat >buildmod/Makefile <<'EOF'
|
||||
obj-m += cdc_ncm.o
|
||||
ccflags-y += -Wno-error
|
||||
|
||||
all:
|
||||
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
|
||||
|
||||
clean:
|
||||
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
|
||||
EOF
|
||||
|
||||
msg "Building patched module"
|
||||
cd buildmod
|
||||
make -C "$KBUILD" M="$PWD" modules
|
||||
PATCHED_KO="$PWD/cdc_ncm.ko"
|
||||
[[ -f $PATCHED_KO ]] || die "Patched cdc_ncm.ko was not built"
|
||||
|
||||
msg "Patched module info"
|
||||
modinfo "$PATCHED_KO" | sed -n '1,20p'
|
||||
|
||||
msg "Reloading module stack"
|
||||
modprobe -r cdc_mbim 2>/dev/null || true
|
||||
modprobe -r cdc_ncm 2>/dev/null || true
|
||||
insmod "$PATCHED_KO"
|
||||
|
||||
msg "Recent dmesg"
|
||||
dmesg | tail -n 60
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done.
|
||||
|
||||
Now:
|
||||
1. run: sudo dmesg -w
|
||||
2. unplug and replug the USB-C cable to the Mac
|
||||
3. then run:
|
||||
ip -br link
|
||||
lsusb | grep 05ac:1905
|
||||
|
||||
You want to see:
|
||||
- cdc_ncm: Apple 05ac:1905 quirk test module loaded
|
||||
- no bind() failure for 05ac:1905
|
||||
- a new interface appearing
|
||||
|
||||
Workspace:
|
||||
$WORKDIR
|
||||
|
||||
EOF
|
||||
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
PATCHED_CDC_NCM="/root/apple1905-cdcncm/linux-nvidia-6.14-6.14.0/buildmod/cdc_ncm.ko"
|
||||
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "Missing command: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
msg() {
|
||||
printf '\n==> %s\n' "$*"
|
||||
}
|
||||
|
||||
[[ $EUID -eq 0 ]] || {
|
||||
echo "Run with sudo." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
need modprobe
|
||||
need insmod
|
||||
need nmcli
|
||||
need lsusb
|
||||
need ip
|
||||
need awk
|
||||
need grep
|
||||
|
||||
[[ -f $PATCHED_CDC_NCM ]] || {
|
||||
echo "Patched module not found: $PATCHED_CDC_NCM" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
msg "Removing old hard-block config if present"
|
||||
rm -f /etc/modprobe.d/zz-kill-mac-usb-net.conf
|
||||
depmod -a
|
||||
|
||||
msg "Loading USB4 / Type-C / Thunderbolt support"
|
||||
modprobe typec || true
|
||||
modprobe typec_ucsi || true
|
||||
modprobe ucsi_acpi || true
|
||||
modprobe thunderbolt || true
|
||||
modprobe typec_thunderbolt || true
|
||||
modprobe thunderbolt_net || true
|
||||
|
||||
msg "Unloading stock USB network stack"
|
||||
modprobe -r cdc_mbim cdc_wdm cdc_ncm cdc_ether usbnet 2>/dev/null || true
|
||||
|
||||
msg "Loading dependency modules"
|
||||
modprobe usbnet
|
||||
modprobe cdc_ether
|
||||
|
||||
msg "Loading patched cdc_ncm"
|
||||
if lsmod | grep -q '^cdc_ncm '; then
|
||||
echo "cdc_ncm already loaded"
|
||||
else
|
||||
insmod "$PATCHED_CDC_NCM"
|
||||
fi
|
||||
|
||||
msg "Current module state"
|
||||
lsmod | grep -E 'typec|ucsi|thunderbolt|cdc_ncm|cdc_ether|usbnet' || true
|
||||
|
||||
cat <<'EOF'
|
||||
|
||||
Now do this on the Mac:
|
||||
1. Run the Mac script below
|
||||
2. Replug the cable
|
||||
3. Wait for the Mac to land on the fast bus
|
||||
|
||||
Then run this same script again with:
|
||||
sudo ./spark-usbc-setup.sh finalize
|
||||
|
||||
EOF
|
||||
|
||||
if [[ ${1:-} != "finalize" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
msg "Waiting for Apple Mac on fast bus (Bus 004 @ 10000M or equivalent)"
|
||||
for _ in $(seq 1 60); do
|
||||
if lsusb -t | grep -q '05ac:1905\|Driver=\[none\], 10000M'; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
msg "lsusb -t snapshot"
|
||||
lsusb -t
|
||||
echo
|
||||
lsusb | grep '05ac:1905' || true
|
||||
|
||||
msg "Waiting for new Apple USB NICs"
|
||||
for _ in $(seq 1 30); do
|
||||
mapfile -t APPLE_IFS < <(
|
||||
ip -o link | awk -F': ' '{print $2}' |
|
||||
grep '^enx36be1bab12' || true
|
||||
)
|
||||
if [[ ${#APPLE_IFS[@]} -ge 2 ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ ${#APPLE_IFS[@]} -lt 2 ]]; then
|
||||
echo "Did not find two Apple USB interfaces." >&2
|
||||
ip -br link
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IF0="${APPLE_IFS[0]}"
|
||||
IF1="${APPLE_IFS[1]}"
|
||||
|
||||
msg "Found interfaces: $IF0 and $IF1"
|
||||
|
||||
msg "Resetting old NetworkManager profiles"
|
||||
nmcli connection delete mac-usb-dhcp-0 2>/dev/null || true
|
||||
nmcli connection delete mac-usb-dhcp-1 2>/dev/null || true
|
||||
|
||||
msg "Bringing up shared DHCP on both interfaces"
|
||||
nmcli connection add type ethernet ifname "$IF0" con-name mac-usb-dhcp-0
|
||||
nmcli connection modify mac-usb-dhcp-0 \
|
||||
ipv4.method shared \
|
||||
ipv6.method disabled \
|
||||
ipv4.addresses 10.42.0.1/24
|
||||
nmcli connection up mac-usb-dhcp-0
|
||||
|
||||
nmcli connection add type ethernet ifname "$IF1" con-name mac-usb-dhcp-1
|
||||
nmcli connection modify mac-usb-dhcp-1 \
|
||||
ipv4.method shared \
|
||||
ipv6.method disabled \
|
||||
ipv4.addresses 10.43.0.1/24
|
||||
nmcli connection up mac-usb-dhcp-1
|
||||
|
||||
msg "Current addresses"
|
||||
ip -br addr show dev "$IF0"
|
||||
ip -br addr show dev "$IF1"
|
||||
|
||||
msg "Recent NetworkManager log"
|
||||
journalctl -u NetworkManager -n 60 --no-pager || true
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done.
|
||||
|
||||
DGX shared-DHCP interfaces:
|
||||
$IF0 -> 10.42.0.1/24
|
||||
$IF1 -> 10.43.0.1/24
|
||||
|
||||
Next on the Mac:
|
||||
sudo ipconfig set anpi0 DHCP
|
||||
sudo ipconfig set anpi1 DHCP
|
||||
sudo ipconfig set anpi2 DHCP
|
||||
|
||||
EOF
|
||||
Reference in New Issue
Block a user