Compare commits

...

6 Commits

Author SHA1 Message Date
Ryuichi Leo Takashige ce67529524 vibe coding gguf 2026-03-10 18:32:48 +00:00
Ryuichi Leo Takashige c3f3d0eeb9 Vibe coding design baby 2026-03-10 17:21:28 +00:00
Ryuichi Leo Takashige 7199e556ef Some Linux Laptop/Desktop detection and goodbye penguin 2026-03-10 17:03:56 +00:00
Ryuichi Leo Takashige f376c24895 Fix placement preview 2026-03-10 16:55:28 +00:00
Ryuichi Leo Takashige d63277edbd Show Sparks and Linux in topology 2026-03-10 16:08:03 +00:00
Ryuichi Leo Takashige dd121ff966 Fast direct USB connectivity 2026-03-10 16:08:03 +00:00
29 changed files with 1890 additions and 42 deletions
+30
View File
@@ -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) ==="
+292 -2
View File
@@ -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}>
+135 -2
View File
@@ -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
+265
View File
@@ -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]
+13
View File
@@ -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(
+67 -22
View File
@@ -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}")
+88 -2
View File
@@ -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)
+15 -8
View File
@@ -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")
+52
View File
@@ -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"
+180
View File
@@ -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
+155
View File
@@ -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