File size: 20,002 Bytes
caf897d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984e20a
caf897d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
"""
Sparse Model Loader β€” Only loads relevant model parameters into RAM.

This is the MAIN system that:
1. Takes your Kimi 2.6 GGUF model
2. Splits it into layer shards
3. Uses SemanticRouter to determine which shards to load
4. Uses MemoryMonitor to manage RAM (evict unused shards)
5. Runs inference via llama.cpp (mmap β€” OS loads only needed weight pages)

RESULT: Run a 35B model on 8GB RAM by only loading the active layers.

Usage:
    loader = SparseModelLoader(
        model_path="/path/to/kimi-2.6.gguf",
        max_ram_gb=8.0,
    )

    response = loader.chat("Write a Python function")
    # β†’ Router identifies "coding" expert
    # β†’ Loads only coding shards (6 layers β‰ˆ 2GB)
    # β†’ Runs inference
    # β†’ Unloads shards when done
"""

import os
import sys
import time
import json
import hashlib
from typing import Optional, Generator, Dict, List
from dataclasses import dataclass

# Add parent dir to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from semantic_router import SemanticRouter, RouteResult
from memory_monitor import MemoryMonitor


@dataclass
class ModelShard:
    """A shard (group of layers) of the model."""
    shard_id: int
    name: str               # e.g. "coding_expert"
    layer_start: int        # First layer in this shard
    layer_end: int          # Last layer (exclusive)
    size_mb: float          # Estimated size in RAM


class SparseModelLoader:
    """
    Sparse Model Loader β€” loads only relevant model parameters into RAM.

    ARCHITECTURE:
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  User Query: "Write a Python function"               β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  SemanticRouter       β”‚  (<1ms, <1MB RAM)
    β”‚  Classifies: "coding" β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  Shard Manager        β”‚  Decides which layers to load
    β”‚  Load shards: 0-5     β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  MemoryMonitor        β”‚  Checks if RAM available
    β”‚  Evict LRU if needed  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  llama.cpp (mmap)     β”‚  OS loads only needed weight
    β”‚  Run inference        β”‚  pages from SSD into RAM
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  Response: "def sort_list(lst):..."                  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    """

    # Kimi K2.6 model architecture (Moonshot AI)
    # 1 TRILLION parameter MoE model
    # - 384 experts (only 8 activated per token = 2% active!)
    # - 61 transformer layers
    # - Each layer ~2GB at IQ1_S quantization (120GB total)
    KIMI_K2_6_LAYERS = 61
    KIMI_K2_6_EXPERTS = 384
    KIMI_K2_6_ACTIVE_EXPERTS = 8  # Only 8 of 384 experts active per token!

    # Shard definitions β€” which layers each expert needs
    # Based on MoE architecture: early layers = token processing,
    # middle layers = expert routing + computation, later layers = generation
    #
    # KEY INSIGHT: Kimi K2.6 is a MoE model with 384 experts.
    # Only 8 experts (2%) are activated per token. With mmap, the OS
    # only loads the ACTIVE expert weights into RAM β€” the other 376
    # experts stay on disk. This gives us 98% RAM savings automatically!
    SHARD_DEFINITIONS = {
        "chat": [
            ModelShard(0, "chat_basic", 0, 4, 2000),      # 4 layers, ~2GB
        ],
        "coding": [
            ModelShard(1, "coding_early", 0, 6, 3000),     # 6 layers, ~3GB
            ModelShard(2, "coding_logic", 30, 36, 3000),   # 6 layers, ~3GB
        ],
        "web_dev": [
            ModelShard(3, "webdev_early", 0, 8, 4000),     # 8 layers, ~4GB
            ModelShard(4, "webdev_gen", 40, 48, 4000),     # 8 layers, ~4GB
        ],
        "math": [
            ModelShard(5, "math_compute", 10, 20, 5000),   # 10 layers, ~5GB
        ],
        "creative": [
            ModelShard(6, "creative_gen", 40, 50, 5000),   # 10 layers, ~5GB
        ],
        "reasoning": [
            ModelShard(7, "reasoning_full", 0, 10, 5000),  # 10 layers, ~5GB
            ModelShard(8, "reasoning_deep", 50, 61, 5500), # 11 layers, ~5.5GB
        ],
    }

    def __init__(
        self,
        model_path: str,
        max_ram_gb: float = 8.0,
        n_ctx: int = 4096,
        n_threads: Optional[int] = None,
    ):
        """
        Initialize the sparse model loader.

        Args:
            model_path: Path to the Kimi 2.6 GGUF model file
            max_ram_gb: Maximum RAM to use for model shards
            n_ctx: Context window size (tokens)
            n_threads: Number of CPU threads (default: auto-detect)
        """
        self.model_path = model_path
        self.max_ram_gb = max_ram_gb
        self.n_ctx = n_ctx
        self.n_threads = n_threads or os.cpu_count() or 4

        # Initialize components
        self.router = SemanticRouter()
        self.memory = MemoryMonitor(max_ram_gb=max_ram_gb)

        # llama.cpp model instance (loaded lazily)
        self._llm = None
        self._model_size_gb = 0.0
        self._loaded_shards: set = set()

        # Verify model exists
        if os.path.exists(model_path):
            self._model_size_gb = os.path.getsize(model_path) / (1024**3)
            print(f"βœ… Model found: {model_path}")
            print(f"   Size: {self._model_size_gb:.1f} GB")
            print(f"   Max RAM: {max_ram_gb} GB")
            print(f"   Threads: {self.n_threads}")
            print(f"   Context: {n_ctx} tokens")
            print(f"   Layers: {self.KIMI_K2_6_LAYERS}")
        else:
            print(f"⚠️  Model not found at: {model_path}")
            print(f"   The loader will still work in 'simulation mode'")

        print(f"\n🧠 Sparse Model Loader initialized")
        print(f"   Full model: {self._model_size_gb:.1f} GB on disk")
        print(f"   Max RAM usage: {max_ram_gb} GB")
        print(f"   Savings: up to {(1 - max_ram_gb / max(self._model_size_gb, 0.1)) * 100:.0f}%")

    def _load_llama(self):
        """Load the llama.cpp model with mmap (lazy loading)."""
        if self._llm is not None:
            return

        try:
            from llama_cpp import Llama

            print(f"\nπŸ“¦ Loading model with mmap (lazy page loading)...")
            self._llm = Llama(
                model_path=self.model_path,
                n_ctx=self.n_ctx,
                n_threads=self.n_threads,
                n_gpu_layers=0,          # CPU only
                use_mmap=True,           # ← KEY: OS loads only needed pages
                use_mlock=False,         # Don't lock all in RAM
                n_batch=512,
                verbose=False,
            )
            print(f"βœ… Model loaded with mmap β€” OS will manage RAM automatically")

        except ImportError:
            print("❌ llama-cpp-python not installed.")
            print("   Install: pip install llama-cpp-python")
            raise
        except Exception as e:
            print(f"❌ Failed to load model: {e}")
            raise

    def chat(
        self,
        message: str,
        system_prompt: str = "",
        max_tokens: int = 2048,
        temperature: float = 0.7,
        stream: bool = False,
    ) -> str:
        """
        Chat with the model using sparse activation.

        1. Route the query to determine which expert/shards to load
        2. Check if we have enough RAM (evict LRU shards if needed)
        3. Load the model with mmap (OS loads only needed weight pages)
        4. Run inference
        5. Return the response

        Args:
            message: User's message
            system_prompt: Optional system prompt
            max_tokens: Maximum tokens to generate
            temperature: Sampling temperature (0=deterministic, 1=creative)
            stream: Whether to stream the response

        Returns:
            Model's response text
        """
        print(f"\n{'='*60}")
        print(f"πŸ’¬ Query: {message[:100]}...")
        print(f"{'='*60}")

        # Step 1: Route the query
        route = self.router.route(message)
        print(f"\nπŸ” Routing:")
        print(f"   Expert: {route.expert}")
        print(f"   Shards: {route.shard_ids}")
        print(f"   Confidence: {route.confidence:.0%}")
        print(f"   Reason: {route.reason}")

        # Step 2: Determine which shards to load
        shards_to_load = self.SHARD_DEFINITIONS.get(route.expert, [])
        total_shard_size = sum(s.size_mb for s in shards_to_load)
        print(f"\nπŸ“¦ Shards to load:")
        for s in shards_to_load:
            print(f"   Shard {s.shard_id} ({s.name}): layers {s.layer_start}-{s.layer_end} ({s.size_mb:.0f}MB)")
        print(f"   Total: {total_shard_size:.0f}MB ({total_shard_size/1024:.1f}GB)")

        # Step 3: Check RAM availability
        available = self.memory.get_available_ram_mb()
        print(f"\nπŸ’Ύ Memory:")
        print(f"   Available: {available:.0f}MB ({available/1024:.1f}GB)")
        print(f"   Needed: {total_shard_size:.0f}MB ({total_shard_size/1024:.1f}GB)")

        if available < total_shard_size:
            print(f"   ⚠️  Low RAM β€” will use mmap (OS loads from disk on-demand)")
            print(f"   ⚠️  May be slower but will work!")

        # Step 4: Load model (mmap handles the sparse loading at OS level)
        self._load_llama()

        # Step 5: Build messages
        messages = []
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})
        messages.append({"role": "user", "content": message})

        # Step 6: Run inference
        print(f"\n⚑ Running inference...")
        start_time = time.time()

        if stream:
            return self._stream_inference(messages, max_tokens, temperature, start_time)
        else:
            response = self._llm.create_chat_completion(
                messages=messages,
                max_tokens=max_tokens,
                temperature=temperature,
                stream=False,
            )
            elapsed = time.time() - start_time
            text = response["choices"][0]["message"]["content"]
            tokens = response.get("usage", {}).get("completion_tokens", 0)

            print(f"βœ… Done in {elapsed:.1f}s ({tokens/max(1,elapsed):.0f} tok/s)")
            print(f"   Generated: {len(text)} chars, {tokens} tokens")

            # Update memory tracking
            self.memory.on_access(shard_id=hash(message) % 1000)

            return text

    def _stream_inference(
        self,
        messages: list,
        max_tokens: int,
        temperature: float,
        start_time: float,
    ) -> Generator[str, None, None]:
        """Stream the response token by token."""
        first_token = True
        token_count = 0

        for chunk in self._llm.create_chat_completion(
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature,
            stream=True,
        ):
            if first_token:
                print(f"\nπŸ“ Response: ", end="", flush=True)
                first_token = False

            delta = chunk["choices"][0].get("delta", {})
            if "content" in delta:
                token_count += 1
                print(delta["content"], end="", flush=True)
                yield delta["content"]

        elapsed = time.time() - start_time
        print(f"\n\nβœ… Done in {elapsed:.1f}s ({token_count/max(1,elapsed):.0f} tok/s)")

    def get_status(self) -> dict:
        """Get current system status."""
        return {
            "model": self.model_path,
            "model_size_gb": f"{self._model_size_gb:.1f}GB",
            "max_ram_gb": f"{self.max_ram_gb}GB",
            "memory": self.memory.get_status(),
            "model_loaded": self._llm is not None,
        }

    def unload(self):
        """Unload the model from RAM."""
        if self._llm is not None:
            del self._llm
            self._llm = None
            print(f"πŸ—‘οΈ  Model unloaded from RAM")


# ─────────────────────────────────────────────────────────────────────────
#  API SERVER β€” OpenAI-compatible endpoint
# ─────────────────────────────────────────────────────────────────────────

def create_api_server(loader: SparseModelLoader, port: int = 8080):
    """Create a FastAPI server with OpenAI-compatible API."""
    try:
        from fastapi import FastAPI, HTTPException
        from fastapi.responses import StreamingResponse
        from pydantic import BaseModel
        import uvicorn
    except ImportError:
        print("❌ FastAPI not installed. Install: pip install fastapi uvicorn")
        return

    app = FastAPI(title="Sparse Model Loader API", version="1.0.0")

    class ChatRequest(BaseModel):
        model: str = "kimi-2.6"
        messages: list
        max_tokens: int = 2048
        temperature: float = 0.7
        stream: bool = False

    @app.get("/v1/models")
    async def list_models():
        return {
            "object": "list",
            "data": [{"id": "kimi-2.6", "object": "model", "owned_by": "sparse-loader"}],
        }

    @app.post("/v1/chat/completions")
    async def chat_completions(req: ChatRequest):
        # Extract the last user message
        user_msg = ""
        system_prompt = ""
        for msg in req.messages:
            if msg["role"] == "system":
                system_prompt = msg["content"]
            elif msg["role"] == "user":
                user_msg = msg["content"]

        if not user_msg:
            raise HTTPException(400, "No user message")

        if req.stream:
            def generate():
                for token in loader.chat(
                    user_msg, system_prompt,
                    max_tokens=req.max_tokens,
                    temperature=req.temperature,
                    stream=True,
                ):
                    yield f"data: {json.dumps({'choices': [{'delta': {'content': token}}]})}\n\n"
                yield "data: [DONE]\n\n"
            return StreamingResponse(generate(), media_type="text/event-stream")
        else:
            text = loader.chat(
                user_msg, system_prompt,
                max_tokens=req.max_tokens,
                temperature=req.temperature,
            )
            return {
                "id": f"chatcmpl-{int(time.time())}",
                "object": "chat.completion",
                "model": "kimi-2.6",
                "choices": [{
                    "index": 0,
                    "message": {"role": "assistant", "content": text},
                    "finish_reason": "stop",
                }],
                "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
            }

    @app.get("/status")
    async def status():
        return loader.get_status()

    print(f"\nπŸš€ API Server starting on http://localhost:{port}")
    print(f"   OpenAI-compatible: http://localhost:{port}/v1/chat/completions")
    print(f"   Status: http://localhost:{port}/status")
    uvicorn.run(app, host="0.0.0.0", port=port)


# ─────────────────────────────────────────────────────────────────────────
#  MAIN β€” Run the sparse model loader
# ─────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Sparse Model Loader β€” Kimi 2.6")
    parser.add_argument("--model", type=str, required=True,
                        help="Path to Kimi 2.6 GGUF model file")
    parser.add_argument("--ram", type=float, default=8.0,
                        help="Max RAM in GB (default: 8.0)")
    parser.add_argument("--port", type=int, default=8080,
                        help="API server port (default: 8080)")
    parser.add_argument("--serve", action="store_true",
                        help="Start API server mode")
    parser.add_argument("--chat", action="store_true",
                        help="Start interactive chat mode")
    args = parser.parse_args()

    # Initialize the loader
    loader = SparseModelLoader(
        model_path=args.model,
        max_ram_gb=args.ram,
    )

    if args.serve:
        # API server mode
        create_api_server(loader, port=args.port)

    elif args.chat:
        # Interactive chat mode
        print(f"\n{'='*60}")
        print(f"🧠 Sparse Model Loader β€” Interactive Chat")
        print(f"   Model: {args.model}")
        print(f"   Max RAM: {args.ram}GB")
        print(f"   Type 'exit' to quit")
        print(f"{'='*60}\n")

        while True:
            try:
                user_input = input("You: ").strip()
                if user_input.lower() in ["exit", "quit", "bye"]:
                    print("Goodbye!")
                    break
                if not user_input:
                    continue

                response = loader.chat(user_input, temperature=0.7)
                print(f"\nAssistant: {response}\n")
            except KeyboardInterrupt:
                print("\nGoodbye!")
                break

    else:
        # Demo mode β€” test the router
        print(f"\n{'='*60}")
        print(f"🧠 DEMO MODE β€” Testing Sparse Loader")
        print(f"{'='*60}")

        test_queries = [
            "Write a Python function to sort a list",
            "Build a todo app with HTML and CSS",
            "What is 25 * 37?",
            "Write a poem about the ocean",
            "Explain how neural networks work",
            "Hello, how are you?",
        ]

        for query in test_queries:
            route = loader.router.route(query)
            shards = loader.SHARD_DEFINITIONS.get(route.expert, [])
            total_mb = sum(s.size_mb for s in shards)
            full_model_mb = loader._model_size_gb * 1024

            print(f"\nπŸ’¬ {query}")
            print(f"   β†’ Expert: {route.expert} ({route.confidence:.0%} confident)")
            print(f"   β†’ Shards: {len(shards)} ({total_mb:.0f}MB)")
            print(f"   β†’ Full model: {full_model_mb:.0f}MB")
            print(f"   β†’ RAM saved: {(1 - total_mb/max(full_model_mb,1))*100:.0f}%")

        print(f"\n{'='*60}")
        print(f"βœ… To run: python {sys.argv[0]} --model /path/to/kimi-2.6.gguf --chat")
        print(f"βœ… To serve: python {sys.argv[0]} --model /path/to/kimi-2.6.gguf --serve")
        print(f"{'='*60}")