digvijay-llm 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Digvijay
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: digvijay_llm
3
+ Version: 0.1.0
4
+ Summary: Run 70B LLMs on low-RAM machines by streaming weights from disk
5
+ Author: Digvijay
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Digvijay
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.9
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: psutil>=5.9.0
32
+ Provides-Extra: gguf
33
+ Requires-Dist: llama-cpp-python>=0.2.0; extra == "gguf"
34
+ Provides-Extra: hf
35
+ Requires-Dist: torch>=2.0.0; extra == "hf"
36
+ Requires-Dist: transformers>=4.38.0; extra == "hf"
37
+ Requires-Dist: accelerate>=0.27.0; extra == "hf"
38
+ Requires-Dist: safetensors>=0.4.0; extra == "hf"
39
+ Provides-Extra: all
40
+ Requires-Dist: llama-cpp-python>=0.2.0; extra == "all"
41
+ Requires-Dist: torch>=2.0.0; extra == "all"
42
+ Requires-Dist: transformers>=4.38.0; extra == "all"
43
+ Requires-Dist: accelerate>=0.27.0; extra == "all"
44
+ Requires-Dist: safetensors>=0.4.0; extra == "all"
45
+ Dynamic: author
46
+ Dynamic: license-file
47
+ Dynamic: requires-python
48
+
49
+ # digvijay_llm
50
+
51
+ Run big open-source LLMs — including 70B-class models — on machines with as little as **16 GB of RAM**, by streaming weights from disk ("ROM") instead of loading the full model into RAM.
52
+
53
+ ## ⚠️ Read this first (important honesty note)
54
+
55
+ A CPU/GPU can only *compute* on data sitting in RAM/VRAM — it is physically impossible to run matrix multiplications directly on a disk. What this library actually does, and what makes low-RAM 70B inference possible, is **disk-backed weight streaming**: weights live on disk and are pulled into RAM only for the layer currently being computed, then released. Peak RAM usage becomes roughly "one layer's worth of weights + activations" instead of "the entire model's weights." The tradeoff is speed — disk I/O is much slower than RAM, so expect lower tokens/sec than a fully-in-RAM setup, especially on HDDs. An NVMe SSD is strongly recommended.
56
+
57
+ ## How it works
58
+
59
+ `digvijay_llm` ships two backends:
60
+
61
+ | Backend | File format | Mechanism | Best for |
62
+ |---|---|---|---|
63
+ | `GGUFStreamingEngine` | `.gguf` (quantized) | Wraps `llama.cpp` via `llama-cpp-python`, using `mmap()` so the OS pages weights in/out of RAM on demand | Production use — fastest, most memory-efficient, supports 4-bit/5-bit quantization |
64
+ | `SafetensorsLayerStreamer` | raw HuggingFace `.safetensors` checkpoints | Builds the model with empty weights, then uses `accelerate`'s disk-offload hooks to load **one transformer layer at a time** from disk right before it's needed | When you only have an unconverted HF checkpoint and don't want to quantize/convert to GGUF first |
65
+
66
+ Both are wrapped behind one simple class, `LowRAMLLM`, so your code doesn't need to care which backend is active.
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ pip install -r requirements.txt
72
+ # or, pick only what you need:
73
+ pip install digvijay_llm[gguf] # GGUF backend only (lighter install)
74
+ pip install digvijay_llm[hf] # safetensors backend only
75
+ pip install digvijay_llm[all] # both
76
+ ```
77
+
78
+ ## Quick start
79
+
80
+ ### Option A — GGUF model (recommended, fastest)
81
+
82
+ ```python
83
+ from digvijay_llm import LowRAMLLM
84
+
85
+ llm = LowRAMLLM.from_gguf("models/llama-3-70b.Q4_K_M.gguf", n_ram_gb=16)
86
+ print(llm.generate("Explain quantum entanglement simply."))
87
+
88
+ # streaming token-by-token
89
+ for token in llm.stream("Write a short poem about the sea."):
90
+ print(token, end="", flush=True)
91
+
92
+ # chat interface
93
+ reply = llm.chat([{"role": "user", "content": "Hello!"}])
94
+ print(reply)
95
+ ```
96
+
97
+ Get a `.gguf` file either by quantizing a model yourself with `llama.cpp`'s `convert_hf_to_gguf.py` + `llama-quantize`, or by downloading an already-quantized `.gguf` from a model hub.
98
+
99
+ ### Option B — raw HuggingFace safetensors checkpoint (no conversion needed)
100
+
101
+ ```python
102
+ from digvijay_llm import LowRAMLLM
103
+
104
+ llm = LowRAMLLM.from_safetensors("models/llama-3-70b-hf/", n_ram_gb=16)
105
+ print(llm.generate("Write a haiku about the ocean."))
106
+ ```
107
+
108
+ ### Option C — auto-detect
109
+
110
+ ```python
111
+ from digvijay_llm import LowRAMLLM
112
+
113
+ llm = LowRAMLLM.auto("models/llama-3-70b.Q4_K_M.gguf", n_ram_gb=16)
114
+ # or
115
+ llm = LowRAMLLM.auto("models/llama-3-70b-hf/", n_ram_gb=16)
116
+ ```
117
+
118
+ ## Planning your RAM budget
119
+
120
+ ```python
121
+ from digvijay_llm import plan_for_budget
122
+
123
+ plan = plan_for_budget(
124
+ model_path="models/llama-3-70b-hf/",
125
+ total_params_billion=70,
126
+ n_layers=80,
127
+ bytes_per_param=2.0, # fp16
128
+ n_ram_gb=16,
129
+ )
130
+ print(plan)
131
+ for w in plan.warnings:
132
+ print("WARNING:", w)
133
+ ```
134
+
135
+ This estimates how many transformer layers can safely stay resident in RAM at once given your budget, and warns you if your disk doesn't have enough free space for the model or if your RAM is so tight that throughput will suffer badly.
136
+
137
+ ## Practical tips for 70B-on-16GB
138
+
139
+ 1. **Use a quantized GGUF (Q4_K_M or smaller)** — this is by far the biggest lever. A 70B model at Q4 is roughly ~38–40 GB on disk vs ~140 GB at fp16; mmap streaming then only needs to page small chunks at a time.
140
+ 2. **Use an NVMe SSD**, not a spinning HDD or network drive — random-access read speed directly determines your tokens/sec.
141
+ 3. **Keep context length modest** (the KV cache also eats RAM) — `digvijay_llm` auto-shrinks context size on tight budgets, but you can override it.
142
+ 4. **Close other RAM-heavy apps** while running — the OS needs free RAM to use as page cache for the mmap'd weights.
143
+ 5. **`n_gpu_layers`** — if you have even a small GPU, offloading a few layers to VRAM (`n_gpu_layers=N`) reduces disk pressure further.
144
+
145
+ ## Project layout
146
+
147
+ ```
148
+ digvijay_llm/
149
+ ├── __init__.py # public API surface
150
+ ├── api.py # LowRAMLLM unified class
151
+ ├── engine_gguf.py # llama.cpp mmap-based backend
152
+ ├── engine_safetensors.py # accelerate disk-offload layer-streaming backend
153
+ └── ram_planner.py # RAM budget heuristics
154
+ examples/
155
+ ├── run_gguf_example.py
156
+ └── run_safetensors_example.py
157
+ tests/
158
+ └── test_ram_planner.py
159
+ ```
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,115 @@
1
+ # digvijay_llm
2
+
3
+ Run big open-source LLMs — including 70B-class models — on machines with as little as **16 GB of RAM**, by streaming weights from disk ("ROM") instead of loading the full model into RAM.
4
+
5
+ ## ⚠️ Read this first (important honesty note)
6
+
7
+ A CPU/GPU can only *compute* on data sitting in RAM/VRAM — it is physically impossible to run matrix multiplications directly on a disk. What this library actually does, and what makes low-RAM 70B inference possible, is **disk-backed weight streaming**: weights live on disk and are pulled into RAM only for the layer currently being computed, then released. Peak RAM usage becomes roughly "one layer's worth of weights + activations" instead of "the entire model's weights." The tradeoff is speed — disk I/O is much slower than RAM, so expect lower tokens/sec than a fully-in-RAM setup, especially on HDDs. An NVMe SSD is strongly recommended.
8
+
9
+ ## How it works
10
+
11
+ `digvijay_llm` ships two backends:
12
+
13
+ | Backend | File format | Mechanism | Best for |
14
+ |---|---|---|---|
15
+ | `GGUFStreamingEngine` | `.gguf` (quantized) | Wraps `llama.cpp` via `llama-cpp-python`, using `mmap()` so the OS pages weights in/out of RAM on demand | Production use — fastest, most memory-efficient, supports 4-bit/5-bit quantization |
16
+ | `SafetensorsLayerStreamer` | raw HuggingFace `.safetensors` checkpoints | Builds the model with empty weights, then uses `accelerate`'s disk-offload hooks to load **one transformer layer at a time** from disk right before it's needed | When you only have an unconverted HF checkpoint and don't want to quantize/convert to GGUF first |
17
+
18
+ Both are wrapped behind one simple class, `LowRAMLLM`, so your code doesn't need to care which backend is active.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install -r requirements.txt
24
+ # or, pick only what you need:
25
+ pip install digvijay_llm[gguf] # GGUF backend only (lighter install)
26
+ pip install digvijay_llm[hf] # safetensors backend only
27
+ pip install digvijay_llm[all] # both
28
+ ```
29
+
30
+ ## Quick start
31
+
32
+ ### Option A — GGUF model (recommended, fastest)
33
+
34
+ ```python
35
+ from digvijay_llm import LowRAMLLM
36
+
37
+ llm = LowRAMLLM.from_gguf("models/llama-3-70b.Q4_K_M.gguf", n_ram_gb=16)
38
+ print(llm.generate("Explain quantum entanglement simply."))
39
+
40
+ # streaming token-by-token
41
+ for token in llm.stream("Write a short poem about the sea."):
42
+ print(token, end="", flush=True)
43
+
44
+ # chat interface
45
+ reply = llm.chat([{"role": "user", "content": "Hello!"}])
46
+ print(reply)
47
+ ```
48
+
49
+ Get a `.gguf` file either by quantizing a model yourself with `llama.cpp`'s `convert_hf_to_gguf.py` + `llama-quantize`, or by downloading an already-quantized `.gguf` from a model hub.
50
+
51
+ ### Option B — raw HuggingFace safetensors checkpoint (no conversion needed)
52
+
53
+ ```python
54
+ from digvijay_llm import LowRAMLLM
55
+
56
+ llm = LowRAMLLM.from_safetensors("models/llama-3-70b-hf/", n_ram_gb=16)
57
+ print(llm.generate("Write a haiku about the ocean."))
58
+ ```
59
+
60
+ ### Option C — auto-detect
61
+
62
+ ```python
63
+ from digvijay_llm import LowRAMLLM
64
+
65
+ llm = LowRAMLLM.auto("models/llama-3-70b.Q4_K_M.gguf", n_ram_gb=16)
66
+ # or
67
+ llm = LowRAMLLM.auto("models/llama-3-70b-hf/", n_ram_gb=16)
68
+ ```
69
+
70
+ ## Planning your RAM budget
71
+
72
+ ```python
73
+ from digvijay_llm import plan_for_budget
74
+
75
+ plan = plan_for_budget(
76
+ model_path="models/llama-3-70b-hf/",
77
+ total_params_billion=70,
78
+ n_layers=80,
79
+ bytes_per_param=2.0, # fp16
80
+ n_ram_gb=16,
81
+ )
82
+ print(plan)
83
+ for w in plan.warnings:
84
+ print("WARNING:", w)
85
+ ```
86
+
87
+ This estimates how many transformer layers can safely stay resident in RAM at once given your budget, and warns you if your disk doesn't have enough free space for the model or if your RAM is so tight that throughput will suffer badly.
88
+
89
+ ## Practical tips for 70B-on-16GB
90
+
91
+ 1. **Use a quantized GGUF (Q4_K_M or smaller)** — this is by far the biggest lever. A 70B model at Q4 is roughly ~38–40 GB on disk vs ~140 GB at fp16; mmap streaming then only needs to page small chunks at a time.
92
+ 2. **Use an NVMe SSD**, not a spinning HDD or network drive — random-access read speed directly determines your tokens/sec.
93
+ 3. **Keep context length modest** (the KV cache also eats RAM) — `digvijay_llm` auto-shrinks context size on tight budgets, but you can override it.
94
+ 4. **Close other RAM-heavy apps** while running — the OS needs free RAM to use as page cache for the mmap'd weights.
95
+ 5. **`n_gpu_layers`** — if you have even a small GPU, offloading a few layers to VRAM (`n_gpu_layers=N`) reduces disk pressure further.
96
+
97
+ ## Project layout
98
+
99
+ ```
100
+ digvijay_llm/
101
+ ├── __init__.py # public API surface
102
+ ├── api.py # LowRAMLLM unified class
103
+ ├── engine_gguf.py # llama.cpp mmap-based backend
104
+ ├── engine_safetensors.py # accelerate disk-offload layer-streaming backend
105
+ └── ram_planner.py # RAM budget heuristics
106
+ examples/
107
+ ├── run_gguf_example.py
108
+ └── run_safetensors_example.py
109
+ tests/
110
+ └── test_ram_planner.py
111
+ ```
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,62 @@
1
+ """
2
+ digvijay_llm
3
+ ============
4
+
5
+ Run large open-source LLMs (including 70B-class models) on machines with
6
+ very limited RAM (e.g. 16 GB) by NEVER loading the full weight set into
7
+ memory at once. Instead, weights stay on disk (your "ROM" — SSD/HDD) and
8
+ are streamed in just-in-time, layer by layer, then released.
9
+
10
+ IMPORTANT HONEST NOTE (please read):
11
+ -------------------------------------
12
+ It is physically impossible to *compute* directly on disk/ROM — the CPU/GPU
13
+ can only execute operations on data that is in RAM/VRAM. What IS possible,
14
+ and what this library does, is keep the bulk of the model's weights on disk
15
+ and only pull small pieces (one layer / one tensor at a time) into RAM right
16
+ before they're needed, then drop them again. This keeps peak RAM usage close
17
+ to "one layer's worth of weights" instead of "the whole model's worth",
18
+ which is what makes running a 70B model on a 16GB machine feasible (at the
19
+ cost of disk I/O speed -> slower tokens/sec, especially on HDD; an NVMe SSD
20
+ is strongly recommended).
21
+
22
+ Two backends are provided:
23
+
24
+ 1. GGUFStreamingEngine
25
+ - Wraps llama.cpp (via llama-cpp-python) which already implements
26
+ production-grade memory-mapped (mmap) weight streaming + quantization.
27
+ - Recommended for real, fast usage. Requires a .gguf quantized model file.
28
+
29
+ 2. SafetensorsLayerStreamer
30
+ - A pure PyTorch engine written from scratch in this library that loads
31
+ a HuggingFace-format (.safetensors) checkpoint ONE TRANSFORMER LAYER
32
+ AT A TIME directly from disk, runs the forward pass for that layer,
33
+ then frees it before loading the next layer.
34
+ - Useful when you only have raw HF weights and don't want to convert to
35
+ GGUF, or want full Python-level control over the streaming behavior.
36
+
37
+ Quick start
38
+ -----------
39
+ from digvijay_llm import LowRAMLLM
40
+
41
+ llm = LowRAMLLM.from_gguf("models/llama-70b.Q4_K_M.gguf", n_ram_gb=16)
42
+ print(llm.generate("Explain quantum entanglement simply."))
43
+
44
+ # or, for raw HF safetensors checkpoints:
45
+ llm = LowRAMLLM.from_safetensors("models/llama-70b-hf/")
46
+ print(llm.generate("Write a haiku about the ocean."))
47
+ """
48
+
49
+ from .engine_gguf import GGUFStreamingEngine
50
+ from .engine_safetensors import SafetensorsLayerStreamer
51
+ from .api import LowRAMLLM
52
+ from .ram_planner import RAMPlanner, plan_for_budget
53
+
54
+ __all__ = [
55
+ "LowRAMLLM",
56
+ "GGUFStreamingEngine",
57
+ "SafetensorsLayerStreamer",
58
+ "RAMPlanner",
59
+ "plan_for_budget",
60
+ ]
61
+
62
+ __version__ = "0.1.0"
@@ -0,0 +1,101 @@
1
+ """
2
+ api.py
3
+ ======
4
+ The single class most users will actually import: `LowRAMLLM`.
5
+ Picks the right backend (GGUF mmap streaming vs safetensors layer
6
+ streaming) based on what's passed in, and exposes one consistent
7
+ .generate() / .stream() / .chat() interface regardless of backend.
8
+ """
9
+
10
+ import os
11
+ from typing import Optional, Iterator, List, Dict
12
+
13
+
14
+ class LowRAMLLM:
15
+ """
16
+ Unified entrypoint for digvijay_llm.
17
+
18
+ Use the classmethods to construct it:
19
+ LowRAMLLM.from_gguf(path, n_ram_gb=16)
20
+ LowRAMLLM.from_safetensors(dir_path, n_ram_gb=16)
21
+ LowRAMLLM.auto(path_or_dir, n_ram_gb=16) # detects which backend to use
22
+ """
23
+
24
+ def __init__(self, engine, backend: str):
25
+ self._engine = engine
26
+ self.backend = backend
27
+
28
+ # ---------- constructors ----------
29
+
30
+ @classmethod
31
+ def from_gguf(
32
+ cls,
33
+ model_path: str,
34
+ n_ram_gb: float = 16.0,
35
+ n_gpu_layers: int = 0,
36
+ n_threads: Optional[int] = None,
37
+ verbose: bool = False,
38
+ ) -> "LowRAMLLM":
39
+ from .engine_gguf import GGUFStreamingEngine
40
+
41
+ engine = GGUFStreamingEngine(
42
+ model_path=model_path,
43
+ n_ram_gb=n_ram_gb,
44
+ n_gpu_layers=n_gpu_layers,
45
+ n_threads=n_threads,
46
+ verbose=verbose,
47
+ )
48
+ return cls(engine, backend="gguf-mmap")
49
+
50
+ @classmethod
51
+ def from_safetensors(
52
+ cls,
53
+ model_dir: str,
54
+ offload_dir: Optional[str] = None,
55
+ n_ram_gb: float = 16.0,
56
+ dtype: str = "float16",
57
+ ) -> "LowRAMLLM":
58
+ from .engine_safetensors import SafetensorsLayerStreamer
59
+
60
+ engine = SafetensorsLayerStreamer(
61
+ model_dir=model_dir,
62
+ offload_dir=offload_dir,
63
+ n_ram_gb=n_ram_gb,
64
+ dtype=dtype,
65
+ )
66
+ return cls(engine, backend="safetensors-layer-stream")
67
+
68
+ @classmethod
69
+ def auto(cls, path: str, n_ram_gb: float = 16.0, **kwargs) -> "LowRAMLLM":
70
+ """Detects whether `path` is a .gguf file or an HF checkpoint dir."""
71
+ if os.path.isfile(path) and path.endswith(".gguf"):
72
+ return cls.from_gguf(path, n_ram_gb=n_ram_gb, **kwargs)
73
+ if os.path.isdir(path):
74
+ has_st = any(f.endswith(".safetensors") for f in os.listdir(path))
75
+ if has_st:
76
+ return cls.from_safetensors(path, n_ram_gb=n_ram_gb, **kwargs)
77
+ raise ValueError(
78
+ f"Could not detect model type at '{path}'. Expected a .gguf file "
79
+ "or a directory containing .safetensors shards."
80
+ )
81
+
82
+ # ---------- inference ----------
83
+
84
+ def generate(self, prompt: str, max_tokens: int = 256, temperature: float = 0.7, **kwargs) -> str:
85
+ return self._engine.generate(prompt, max_tokens=max_tokens, temperature=temperature, **kwargs)
86
+
87
+ def stream(self, prompt: str, max_tokens: int = 256, temperature: float = 0.7) -> Iterator[str]:
88
+ if self.backend == "gguf-mmap":
89
+ yield from self._engine.generate(prompt, max_tokens=max_tokens, temperature=temperature, stream=True)
90
+ else:
91
+ yield from self._engine.stream_generate(prompt, max_tokens=max_tokens, temperature=temperature)
92
+
93
+ def chat(self, messages: List[Dict[str, str]], max_tokens: int = 256, temperature: float = 0.7) -> str:
94
+ if self.backend == "gguf-mmap":
95
+ return self._engine.chat(messages, max_tokens=max_tokens, temperature=temperature)
96
+ # Fallback for safetensors backend: flatten chat messages into a prompt
97
+ prompt = "\n".join(f"{m['role']}: {m['content']}" for m in messages) + "\nassistant:"
98
+ return self._engine.generate(prompt, max_tokens=max_tokens, temperature=temperature)
99
+
100
+ def __repr__(self):
101
+ return f"<LowRAMLLM backend={self.backend!r}>"
@@ -0,0 +1,125 @@
1
+ """
2
+ engine_gguf.py
3
+ ==============
4
+ Production-grade low-RAM backend. Wraps llama-cpp-python, which loads
5
+ GGUF files via mmap() — the OS keeps weights on disk and pages them into
6
+ RAM lazily as the model touches them, evicting cold pages under memory
7
+ pressure. Combined with quantization (Q4_K_M, Q5_K_M, etc.) this is what
8
+ lets a 70B model run on a 16GB machine.
9
+
10
+ This module just gives that capability a clean, documented entrypoint
11
+ inside digvijay_llm, with sane low-RAM defaults (no mlock, limited
12
+ context, mmap forced on, conservative thread count).
13
+ """
14
+
15
+ import os
16
+ from typing import Optional, Iterator
17
+
18
+
19
+ class GGUFStreamingEngine:
20
+ """
21
+ Streams a quantized .gguf model off disk instead of loading it fully
22
+ into RAM, via memory-mapping (mmap).
23
+
24
+ Parameters
25
+ ----------
26
+ model_path : str
27
+ Path to a .gguf quantized model file (e.g. produced by
28
+ llama.cpp's convert + quantize scripts, or downloaded pre-quantized
29
+ from a hub like Hugging Face / TheBloke-style repos).
30
+ n_ram_gb : float
31
+ Approximate RAM budget; used only to pick a sane default context
32
+ size / batch size so you don't accidentally request a KV cache
33
+ bigger than your machine can hold.
34
+ n_gpu_layers : int
35
+ Number of layers to offload to GPU if you have one (0 = pure CPU+
36
+ disk streaming, which is the "low RAM" use case this library is
37
+ built for).
38
+ n_threads : Optional[int]
39
+ CPU threads to use. Defaults to os.cpu_count().
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ model_path: str,
45
+ n_ram_gb: float = 16.0,
46
+ n_gpu_layers: int = 0,
47
+ n_threads: Optional[int] = None,
48
+ verbose: bool = False,
49
+ ):
50
+ if not os.path.exists(model_path):
51
+ raise FileNotFoundError(f"GGUF model not found at: {model_path}")
52
+
53
+ try:
54
+ from llama_cpp import Llama
55
+ except ImportError as e:
56
+ raise ImportError(
57
+ "engine_gguf requires the 'llama-cpp-python' package. "
58
+ "Install it with: pip install llama-cpp-python"
59
+ ) from e
60
+
61
+ self.model_path = model_path
62
+ self.n_ram_gb = n_ram_gb
63
+
64
+ # Conservative context size: bigger context = bigger KV cache = more
65
+ # RAM. We scale it down automatically on tight budgets.
66
+ if n_ram_gb <= 8:
67
+ ctx = 1024
68
+ elif n_ram_gb <= 16:
69
+ ctx = 2048
70
+ elif n_ram_gb <= 32:
71
+ ctx = 4096
72
+ else:
73
+ ctx = 8192
74
+
75
+ self._llm = Llama(
76
+ model_path=model_path,
77
+ n_ctx=ctx,
78
+ n_threads=n_threads or os.cpu_count(),
79
+ n_gpu_layers=n_gpu_layers,
80
+ use_mmap=True, # critical: keeps weights disk-backed ("ROM"), not fully copied into RAM
81
+ use_mlock=False, # critical: do NOT force-pin pages into RAM, let the OS evict freely
82
+ verbose=verbose,
83
+ )
84
+ self._ctx_size = ctx
85
+
86
+ def generate(
87
+ self,
88
+ prompt: str,
89
+ max_tokens: int = 256,
90
+ temperature: float = 0.7,
91
+ top_p: float = 0.9,
92
+ stream: bool = False,
93
+ ):
94
+ if stream:
95
+ return self._stream(prompt, max_tokens, temperature, top_p)
96
+
97
+ out = self._llm(
98
+ prompt,
99
+ max_tokens=max_tokens,
100
+ temperature=temperature,
101
+ top_p=top_p,
102
+ )
103
+ return out["choices"][0]["text"]
104
+
105
+ def _stream(self, prompt, max_tokens, temperature, top_p) -> Iterator[str]:
106
+ for chunk in self._llm(
107
+ prompt,
108
+ max_tokens=max_tokens,
109
+ temperature=temperature,
110
+ top_p=top_p,
111
+ stream=True,
112
+ ):
113
+ yield chunk["choices"][0]["text"]
114
+
115
+ def chat(self, messages, max_tokens: int = 256, temperature: float = 0.7):
116
+ """OpenAI-style chat interface, e.g.
117
+ messages=[{"role": "user", "content": "hi"}]"""
118
+ out = self._llm.create_chat_completion(
119
+ messages=messages, max_tokens=max_tokens, temperature=temperature
120
+ )
121
+ return out["choices"][0]["message"]["content"]
122
+
123
+ @property
124
+ def context_size(self) -> int:
125
+ return self._ctx_size
@@ -0,0 +1,175 @@
1
+ """
2
+ engine_safetensors.py
3
+ ======================
4
+ For raw HuggingFace-format checkpoints (.safetensors, no GGUF conversion
5
+ done). This engine loads the model with EMPTY weights first, then attaches
6
+ disk-offload hooks (built on top of Hugging Face `accelerate`) so that:
7
+
8
+ - Only ONE transformer layer's weights are materialized in RAM at a time.
9
+ - Right before a layer's forward() runs, its weights are read from the
10
+ .safetensors shard on disk straight into RAM.
11
+ - Immediately after that layer's forward() finishes, its weights are
12
+ freed from RAM again.
13
+
14
+ This is the "layer-by-layer disk streaming" approach — RAM peak ~= size of
15
+ the largest single layer + activations, instead of the whole model. It is
16
+ slower than keeping everything in RAM (disk I/O per layer per token) but it
17
+ is what makes a 70B model loadable on a 16 GB machine without GGUF
18
+ conversion.
19
+
20
+ Under the hood this uses `accelerate`'s `disk_offload` / `dispatch_model`
21
+ machinery (the same machinery HF itself recommends for low-RAM inference),
22
+ wrapped here with sane defaults + the digvijay_llm.generate() interface.
23
+ """
24
+
25
+ import os
26
+ import gc
27
+ from typing import Optional, Iterator
28
+
29
+
30
+ class SafetensorsLayerStreamer:
31
+ """
32
+ Parameters
33
+ ----------
34
+ model_dir : str
35
+ Path to a local directory containing a HuggingFace checkpoint:
36
+ config.json + one or more *.safetensors weight shards + tokenizer
37
+ files. (Use `huggingface-cli download <repo> --local-dir ...` to
38
+ fetch one without loading it into RAM first.)
39
+ offload_dir : str
40
+ Scratch directory on disk used as the staging area accelerate
41
+ streams weights through. Needs free space roughly equal to the
42
+ model size. Defaults to "<model_dir>/.digvijay_offload".
43
+ n_ram_gb : float
44
+ Approximate RAM budget, used to decide max_memory passed to
45
+ accelerate so it knows how aggressively to offload to disk.
46
+ dtype : str
47
+ "float16" (default, halves RAM vs float32) or "float32".
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ model_dir: str,
53
+ offload_dir: Optional[str] = None,
54
+ n_ram_gb: float = 16.0,
55
+ dtype: str = "float16",
56
+ device: str = "cpu",
57
+ ):
58
+ if not os.path.isdir(model_dir):
59
+ raise FileNotFoundError(f"model_dir not found: {model_dir}")
60
+
61
+ try:
62
+ import torch
63
+ from transformers import AutoModelForCausalLM, AutoTokenizer
64
+ from accelerate import infer_auto_device_map, dispatch_model, init_empty_weights
65
+ from accelerate.utils import get_balanced_memory
66
+ except ImportError as e:
67
+ raise ImportError(
68
+ "engine_safetensors requires 'torch', 'transformers', and "
69
+ "'accelerate'. Install with: "
70
+ "pip install torch transformers accelerate safetensors"
71
+ ) from e
72
+
73
+ self._torch = torch
74
+ self.model_dir = model_dir
75
+ self.offload_dir = offload_dir or os.path.join(model_dir, ".digvijay_offload")
76
+ os.makedirs(self.offload_dir, exist_ok=True)
77
+
78
+ torch_dtype = torch.float16 if dtype == "float16" else torch.float32
79
+
80
+ # 1. Build the model architecture with NO weights materialized yet
81
+ # (this costs ~0 RAM regardless of model size).
82
+ from transformers import AutoConfig
83
+ config = AutoConfig.from_pretrained(model_dir)
84
+ with init_empty_weights():
85
+ empty_model = AutoModelForCausalLM.from_config(config, torch_dtype=torch_dtype)
86
+
87
+ # 2. Reserve almost all RAM for ONE layer at a time by capping the
88
+ # "max_memory" accelerate is allowed to use on this device; the
89
+ # rest is automatically offloaded to `offload_dir` on disk and
90
+ # streamed back in layer-by-layer during forward().
91
+ reserved_for_layer_gb = max(1.0, n_ram_gb * 0.5) # heuristic safety margin
92
+ max_memory = {device: f"{reserved_for_layer_gb}GiB", "cpu": f"{reserved_for_layer_gb}GiB"}
93
+
94
+ device_map = infer_auto_device_map(
95
+ empty_model,
96
+ max_memory=max_memory,
97
+ no_split_module_classes=self._guess_no_split_classes(empty_model),
98
+ dtype=torch_dtype,
99
+ )
100
+
101
+ # 3. Load real weights directly from the safetensors shards on disk
102
+ # INTO the device_map plan above — anything that doesn't fit the
103
+ # RAM budget gets a disk-offload hook instead of being loaded.
104
+ from accelerate import load_checkpoint_and_dispatch
105
+
106
+ self._model = load_checkpoint_and_dispatch(
107
+ empty_model,
108
+ checkpoint=model_dir,
109
+ device_map=device_map,
110
+ offload_folder=self.offload_dir,
111
+ offload_state_dict=True, # streams shards in piece by piece while loading too
112
+ dtype=torch_dtype,
113
+ )
114
+ self._model.eval()
115
+
116
+ self._tokenizer = AutoTokenizer.from_pretrained(model_dir)
117
+ self.device_map = device_map
118
+
119
+ @staticmethod
120
+ def _guess_no_split_classes(model):
121
+ """Find the repeated transformer-block class so accelerate never
122
+ splits a single decoder layer's weights across two offload
123
+ locations (which would break the "one layer in RAM at a time"
124
+ guarantee)."""
125
+ names = set()
126
+ for module in model.modules():
127
+ cls_name = module.__class__.__name__
128
+ if "Layer" in cls_name or "Block" in cls_name:
129
+ names.add(cls_name)
130
+ return list(names) if names else None
131
+
132
+ def generate(
133
+ self,
134
+ prompt: str,
135
+ max_tokens: int = 256,
136
+ temperature: float = 0.7,
137
+ top_p: float = 0.9,
138
+ ) -> str:
139
+ torch = self._torch
140
+ inputs = self._tokenizer(prompt, return_tensors="pt")
141
+ with torch.no_grad():
142
+ output_ids = self._model.generate(
143
+ **inputs,
144
+ max_new_tokens=max_tokens,
145
+ do_sample=temperature > 0,
146
+ temperature=max(temperature, 1e-5),
147
+ top_p=top_p,
148
+ pad_token_id=self._tokenizer.eos_token_id,
149
+ )
150
+ text = self._tokenizer.decode(output_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
151
+ gc.collect()
152
+ return text
153
+
154
+ def stream_generate(self, prompt: str, max_tokens: int = 256, temperature: float = 0.7) -> Iterator[str]:
155
+ """Token-by-token streaming using transformers' TextIteratorStreamer."""
156
+ from transformers import TextIteratorStreamer
157
+ import threading
158
+
159
+ torch = self._torch
160
+ inputs = self._tokenizer(prompt, return_tensors="pt")
161
+ streamer = TextIteratorStreamer(self._tokenizer, skip_prompt=True, skip_special_tokens=True)
162
+
163
+ gen_kwargs = dict(
164
+ **inputs,
165
+ max_new_tokens=max_tokens,
166
+ do_sample=temperature > 0,
167
+ temperature=max(temperature, 1e-5),
168
+ streamer=streamer,
169
+ pad_token_id=self._tokenizer.eos_token_id,
170
+ )
171
+ thread = threading.Thread(target=self._model.generate, kwargs=gen_kwargs)
172
+ thread.start()
173
+ for new_text in streamer:
174
+ yield new_text
175
+ thread.join()
@@ -0,0 +1,105 @@
1
+ """
2
+ ram_planner.py
3
+ ==============
4
+ Helps decide HOW to stream a model given a target RAM budget: how many
5
+ layers (if any) can be safely kept "hot" in RAM at once, how big the
6
+ on-disk page cache window should be, and whether the user's requested
7
+ model will even fit on disk + meet a minimum tokens/sec expectation.
8
+
9
+ This is intentionally simple/heuristic — its job is to stop people from
10
+ footgunning themselves (e.g. trying to keep 40 layers "hot" on a 16GB
11
+ machine) rather than to be a perfect simulator.
12
+ """
13
+
14
+ from dataclasses import dataclass
15
+ import os
16
+ import shutil
17
+
18
+
19
+ @dataclass
20
+ class RAMPlan:
21
+ total_ram_gb: float
22
+ reserved_for_os_gb: float
23
+ usable_ram_gb: float
24
+ safe_hot_layers: int
25
+ layer_size_gb: float
26
+ disk_free_gb: float
27
+ warnings: list
28
+
29
+
30
+ def _disk_free_gb(path: str) -> float:
31
+ try:
32
+ usage = shutil.disk_usage(path if os.path.isdir(path) else os.path.dirname(path) or ".")
33
+ return usage.free / (1024 ** 3)
34
+ except Exception:
35
+ return float("inf")
36
+
37
+
38
+ def plan_for_budget(
39
+ model_path: str,
40
+ total_params_billion: float,
41
+ n_layers: int,
42
+ bytes_per_param: float = 2.0, # fp16=2, 8bit=1, 4bit~=0.5-0.6
43
+ n_ram_gb: float = 16.0,
44
+ os_reserve_gb: float = 2.5,
45
+ ) -> RAMPlan:
46
+ """
47
+ Compute a conservative streaming plan: how many transformer layers can
48
+ be kept resident in RAM simultaneously without exceeding the budget.
49
+ """
50
+ warnings = []
51
+ usable = max(n_ram_gb - os_reserve_gb, 1.0)
52
+
53
+ total_weight_gb = (total_params_billion * 1e9 * bytes_per_param) / (1024 ** 3)
54
+ layer_size_gb = total_weight_gb / max(n_layers, 1)
55
+
56
+ # Keep a safety margin for activations / KV cache / runtime overhead
57
+ activation_overhead_gb = max(0.5, usable * 0.15)
58
+ budget_for_weights = max(usable - activation_overhead_gb, 0.5)
59
+
60
+ safe_hot_layers = max(1, int(budget_for_weights // layer_size_gb))
61
+ if safe_hot_layers < 2:
62
+ warnings.append(
63
+ "Budget is extremely tight: only 1 layer fits in RAM at a time. "
64
+ "Expect slow throughput; an NVMe SSD is strongly recommended."
65
+ )
66
+
67
+ disk_free = _disk_free_gb(model_path)
68
+ if disk_free < total_weight_gb:
69
+ warnings.append(
70
+ f"Model needs ~{total_weight_gb:.1f} GB on disk but only "
71
+ f"{disk_free:.1f} GB free was detected at '{model_path}'."
72
+ )
73
+
74
+ return RAMPlan(
75
+ total_ram_gb=n_ram_gb,
76
+ reserved_for_os_gb=os_reserve_gb,
77
+ usable_ram_gb=usable,
78
+ safe_hot_layers=safe_hot_layers,
79
+ layer_size_gb=layer_size_gb,
80
+ disk_free_gb=disk_free,
81
+ warnings=warnings,
82
+ )
83
+
84
+
85
+ class RAMPlanner:
86
+ """Thin OO wrapper kept for ergonomic imports: RAMPlanner(...).plan()"""
87
+
88
+ def __init__(self, model_path: str, total_params_billion: float, n_layers: int,
89
+ bytes_per_param: float = 2.0, n_ram_gb: float = 16.0, os_reserve_gb: float = 2.5):
90
+ self.model_path = model_path
91
+ self.total_params_billion = total_params_billion
92
+ self.n_layers = n_layers
93
+ self.bytes_per_param = bytes_per_param
94
+ self.n_ram_gb = n_ram_gb
95
+ self.os_reserve_gb = os_reserve_gb
96
+
97
+ def plan(self) -> RAMPlan:
98
+ return plan_for_budget(
99
+ self.model_path,
100
+ self.total_params_billion,
101
+ self.n_layers,
102
+ self.bytes_per_param,
103
+ self.n_ram_gb,
104
+ self.os_reserve_gb,
105
+ )
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: digvijay_llm
3
+ Version: 0.1.0
4
+ Summary: Run 70B LLMs on low-RAM machines by streaming weights from disk
5
+ Author: Digvijay
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Digvijay
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.9
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: psutil>=5.9.0
32
+ Provides-Extra: gguf
33
+ Requires-Dist: llama-cpp-python>=0.2.0; extra == "gguf"
34
+ Provides-Extra: hf
35
+ Requires-Dist: torch>=2.0.0; extra == "hf"
36
+ Requires-Dist: transformers>=4.38.0; extra == "hf"
37
+ Requires-Dist: accelerate>=0.27.0; extra == "hf"
38
+ Requires-Dist: safetensors>=0.4.0; extra == "hf"
39
+ Provides-Extra: all
40
+ Requires-Dist: llama-cpp-python>=0.2.0; extra == "all"
41
+ Requires-Dist: torch>=2.0.0; extra == "all"
42
+ Requires-Dist: transformers>=4.38.0; extra == "all"
43
+ Requires-Dist: accelerate>=0.27.0; extra == "all"
44
+ Requires-Dist: safetensors>=0.4.0; extra == "all"
45
+ Dynamic: author
46
+ Dynamic: license-file
47
+ Dynamic: requires-python
48
+
49
+ # digvijay_llm
50
+
51
+ Run big open-source LLMs — including 70B-class models — on machines with as little as **16 GB of RAM**, by streaming weights from disk ("ROM") instead of loading the full model into RAM.
52
+
53
+ ## ⚠️ Read this first (important honesty note)
54
+
55
+ A CPU/GPU can only *compute* on data sitting in RAM/VRAM — it is physically impossible to run matrix multiplications directly on a disk. What this library actually does, and what makes low-RAM 70B inference possible, is **disk-backed weight streaming**: weights live on disk and are pulled into RAM only for the layer currently being computed, then released. Peak RAM usage becomes roughly "one layer's worth of weights + activations" instead of "the entire model's weights." The tradeoff is speed — disk I/O is much slower than RAM, so expect lower tokens/sec than a fully-in-RAM setup, especially on HDDs. An NVMe SSD is strongly recommended.
56
+
57
+ ## How it works
58
+
59
+ `digvijay_llm` ships two backends:
60
+
61
+ | Backend | File format | Mechanism | Best for |
62
+ |---|---|---|---|
63
+ | `GGUFStreamingEngine` | `.gguf` (quantized) | Wraps `llama.cpp` via `llama-cpp-python`, using `mmap()` so the OS pages weights in/out of RAM on demand | Production use — fastest, most memory-efficient, supports 4-bit/5-bit quantization |
64
+ | `SafetensorsLayerStreamer` | raw HuggingFace `.safetensors` checkpoints | Builds the model with empty weights, then uses `accelerate`'s disk-offload hooks to load **one transformer layer at a time** from disk right before it's needed | When you only have an unconverted HF checkpoint and don't want to quantize/convert to GGUF first |
65
+
66
+ Both are wrapped behind one simple class, `LowRAMLLM`, so your code doesn't need to care which backend is active.
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ pip install -r requirements.txt
72
+ # or, pick only what you need:
73
+ pip install digvijay_llm[gguf] # GGUF backend only (lighter install)
74
+ pip install digvijay_llm[hf] # safetensors backend only
75
+ pip install digvijay_llm[all] # both
76
+ ```
77
+
78
+ ## Quick start
79
+
80
+ ### Option A — GGUF model (recommended, fastest)
81
+
82
+ ```python
83
+ from digvijay_llm import LowRAMLLM
84
+
85
+ llm = LowRAMLLM.from_gguf("models/llama-3-70b.Q4_K_M.gguf", n_ram_gb=16)
86
+ print(llm.generate("Explain quantum entanglement simply."))
87
+
88
+ # streaming token-by-token
89
+ for token in llm.stream("Write a short poem about the sea."):
90
+ print(token, end="", flush=True)
91
+
92
+ # chat interface
93
+ reply = llm.chat([{"role": "user", "content": "Hello!"}])
94
+ print(reply)
95
+ ```
96
+
97
+ Get a `.gguf` file either by quantizing a model yourself with `llama.cpp`'s `convert_hf_to_gguf.py` + `llama-quantize`, or by downloading an already-quantized `.gguf` from a model hub.
98
+
99
+ ### Option B — raw HuggingFace safetensors checkpoint (no conversion needed)
100
+
101
+ ```python
102
+ from digvijay_llm import LowRAMLLM
103
+
104
+ llm = LowRAMLLM.from_safetensors("models/llama-3-70b-hf/", n_ram_gb=16)
105
+ print(llm.generate("Write a haiku about the ocean."))
106
+ ```
107
+
108
+ ### Option C — auto-detect
109
+
110
+ ```python
111
+ from digvijay_llm import LowRAMLLM
112
+
113
+ llm = LowRAMLLM.auto("models/llama-3-70b.Q4_K_M.gguf", n_ram_gb=16)
114
+ # or
115
+ llm = LowRAMLLM.auto("models/llama-3-70b-hf/", n_ram_gb=16)
116
+ ```
117
+
118
+ ## Planning your RAM budget
119
+
120
+ ```python
121
+ from digvijay_llm import plan_for_budget
122
+
123
+ plan = plan_for_budget(
124
+ model_path="models/llama-3-70b-hf/",
125
+ total_params_billion=70,
126
+ n_layers=80,
127
+ bytes_per_param=2.0, # fp16
128
+ n_ram_gb=16,
129
+ )
130
+ print(plan)
131
+ for w in plan.warnings:
132
+ print("WARNING:", w)
133
+ ```
134
+
135
+ This estimates how many transformer layers can safely stay resident in RAM at once given your budget, and warns you if your disk doesn't have enough free space for the model or if your RAM is so tight that throughput will suffer badly.
136
+
137
+ ## Practical tips for 70B-on-16GB
138
+
139
+ 1. **Use a quantized GGUF (Q4_K_M or smaller)** — this is by far the biggest lever. A 70B model at Q4 is roughly ~38–40 GB on disk vs ~140 GB at fp16; mmap streaming then only needs to page small chunks at a time.
140
+ 2. **Use an NVMe SSD**, not a spinning HDD or network drive — random-access read speed directly determines your tokens/sec.
141
+ 3. **Keep context length modest** (the KV cache also eats RAM) — `digvijay_llm` auto-shrinks context size on tight budgets, but you can override it.
142
+ 4. **Close other RAM-heavy apps** while running — the OS needs free RAM to use as page cache for the mmap'd weights.
143
+ 5. **`n_gpu_layers`** — if you have even a small GPU, offloading a few layers to VRAM (`n_gpu_layers=N`) reduces disk pressure further.
144
+
145
+ ## Project layout
146
+
147
+ ```
148
+ digvijay_llm/
149
+ ├── __init__.py # public API surface
150
+ ├── api.py # LowRAMLLM unified class
151
+ ├── engine_gguf.py # llama.cpp mmap-based backend
152
+ ├── engine_safetensors.py # accelerate disk-offload layer-streaming backend
153
+ └── ram_planner.py # RAM budget heuristics
154
+ examples/
155
+ ├── run_gguf_example.py
156
+ └── run_safetensors_example.py
157
+ tests/
158
+ └── test_ram_planner.py
159
+ ```
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ digvijay_llm/__init__.py
6
+ digvijay_llm/api.py
7
+ digvijay_llm/engine_gguf.py
8
+ digvijay_llm/engine_safetensors.py
9
+ digvijay_llm/ram_planner.py
10
+ digvijay_llm.egg-info/PKG-INFO
11
+ digvijay_llm.egg-info/SOURCES.txt
12
+ digvijay_llm.egg-info/dependency_links.txt
13
+ digvijay_llm.egg-info/requires.txt
14
+ digvijay_llm.egg-info/top_level.txt
15
+ tests/test_ram_planner.py
@@ -0,0 +1,17 @@
1
+ psutil>=5.9.0
2
+
3
+ [all]
4
+ llama-cpp-python>=0.2.0
5
+ torch>=2.0.0
6
+ transformers>=4.38.0
7
+ accelerate>=0.27.0
8
+ safetensors>=0.4.0
9
+
10
+ [gguf]
11
+ llama-cpp-python>=0.2.0
12
+
13
+ [hf]
14
+ torch>=2.0.0
15
+ transformers>=4.38.0
16
+ accelerate>=0.27.0
17
+ safetensors>=0.4.0
@@ -0,0 +1 @@
1
+ digvijay_llm
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "digvijay_llm"
7
+ version = "0.1.0"
8
+ description = "Run 70B LLMs on low-RAM machines by streaming weights from disk"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.9"
12
+
13
+ dependencies = [
14
+ "psutil>=5.9.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ gguf = [
19
+ "llama-cpp-python>=0.2.0",
20
+ ]
21
+
22
+ hf = [
23
+ "torch>=2.0.0",
24
+ "transformers>=4.38.0",
25
+ "accelerate>=0.27.0",
26
+ "safetensors>=0.4.0",
27
+ ]
28
+
29
+ all = [
30
+ "llama-cpp-python>=0.2.0",
31
+ "torch>=2.0.0",
32
+ "transformers>=4.38.0",
33
+ "accelerate>=0.27.0",
34
+ "safetensors>=0.4.0",
35
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,33 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="digvijay_llm",
5
+ version="0.1.0",
6
+ description="Run large LLMs (incl. 70B-class) on low-RAM machines by streaming weights from disk instead of holding the full model in RAM.",
7
+ long_description=open("README.md", encoding="utf-8").read() if __import__("os").path.exists("README.md") else "",
8
+ long_description_content_type="text/markdown",
9
+ author="Digvijay",
10
+ license="MIT",
11
+ packages=find_packages(),
12
+ python_requires=">=3.9",
13
+ install_requires=[
14
+ "psutil>=5.9.0",
15
+ ],
16
+ extras_require={
17
+ "gguf": ["llama-cpp-python>=0.2.0"],
18
+ "hf": ["torch>=2.0.0", "transformers>=4.38.0", "accelerate>=0.27.0", "safetensors>=0.4.0"],
19
+ "all": [
20
+ "llama-cpp-python>=0.2.0",
21
+ "torch>=2.0.0",
22
+ "transformers>=4.38.0",
23
+ "accelerate>=0.27.0",
24
+ "safetensors>=0.4.0",
25
+ ],
26
+ },
27
+ classifiers=[
28
+ "Programming Language :: Python :: 3",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Operating System :: OS Independent",
31
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
32
+ ],
33
+ )
@@ -0,0 +1,50 @@
1
+ import os
2
+ import sys
3
+ import unittest
4
+
5
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
6
+
7
+ from digvijay_llm.ram_planner import plan_for_budget, RAMPlanner
8
+
9
+
10
+ class TestRAMPlanner(unittest.TestCase):
11
+ def test_70b_on_16gb_warns_or_streams_safely(self):
12
+ plan = plan_for_budget(
13
+ model_path=".",
14
+ total_params_billion=70,
15
+ n_layers=80,
16
+ bytes_per_param=0.6, # Q4 quantized
17
+ n_ram_gb=16,
18
+ )
19
+ self.assertGreaterEqual(plan.safe_hot_layers, 1)
20
+ self.assertLess(plan.safe_hot_layers, 80) # must NOT fit all layers in RAM
21
+ self.assertGreater(plan.layer_size_gb, 0)
22
+
23
+ def test_small_model_fits_more_layers(self):
24
+ plan = plan_for_budget(
25
+ model_path=".",
26
+ total_params_billion=1.1,
27
+ n_layers=22,
28
+ bytes_per_param=2.0,
29
+ n_ram_gb=16,
30
+ )
31
+ self.assertGreaterEqual(plan.safe_hot_layers, 1)
32
+
33
+ def test_class_wrapper_matches_function(self):
34
+ planner = RAMPlanner(model_path=".", total_params_billion=7, n_layers=32, n_ram_gb=16)
35
+ plan = planner.plan()
36
+ self.assertEqual(plan.total_ram_gb, 16)
37
+
38
+ def test_extreme_budget_emits_warning(self):
39
+ plan = plan_for_budget(
40
+ model_path=".",
41
+ total_params_billion=70,
42
+ n_layers=80,
43
+ bytes_per_param=2.0, # fp16, unrealistic for 16GB -> should warn
44
+ n_ram_gb=4,
45
+ )
46
+ self.assertTrue(len(plan.warnings) >= 1)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ unittest.main()