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.
- digvijay_llm-0.1.0/LICENSE +21 -0
- digvijay_llm-0.1.0/PKG-INFO +163 -0
- digvijay_llm-0.1.0/README.md +115 -0
- digvijay_llm-0.1.0/digvijay_llm/__init__.py +62 -0
- digvijay_llm-0.1.0/digvijay_llm/api.py +101 -0
- digvijay_llm-0.1.0/digvijay_llm/engine_gguf.py +125 -0
- digvijay_llm-0.1.0/digvijay_llm/engine_safetensors.py +175 -0
- digvijay_llm-0.1.0/digvijay_llm/ram_planner.py +105 -0
- digvijay_llm-0.1.0/digvijay_llm.egg-info/PKG-INFO +163 -0
- digvijay_llm-0.1.0/digvijay_llm.egg-info/SOURCES.txt +15 -0
- digvijay_llm-0.1.0/digvijay_llm.egg-info/dependency_links.txt +1 -0
- digvijay_llm-0.1.0/digvijay_llm.egg-info/requires.txt +17 -0
- digvijay_llm-0.1.0/digvijay_llm.egg-info/top_level.txt +1 -0
- digvijay_llm-0.1.0/pyproject.toml +35 -0
- digvijay_llm-0.1.0/setup.cfg +4 -0
- digvijay_llm-0.1.0/setup.py +33 -0
- digvijay_llm-0.1.0/tests/test_ram_planner.py +50 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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()
|