Samat Zharassov commited on
Commit
c37a0e2
·
1 Parent(s): a2003a3

Add cached Hub loading for mini-Jev

Browse files
Files changed (4) hide show
  1. README.md +21 -2
  2. inference.py +33 -1
  3. requirements.txt +1 -0
  4. tests/test_release_loading.py +147 -0
README.md CHANGED
@@ -2,6 +2,8 @@
2
  license: apache-2.0
3
  base_model: Qwen/Qwen3-0.6B
4
  library_name: custom
 
 
5
  tags:
6
  - mini-jev
7
  - decision-model
@@ -49,11 +51,19 @@ The current strength is **finite-choice tool and action selection** from state a
49
 
50
  ## Use
51
 
52
- A CUDA GPU with BF16 support is required for this 4-bit package. Install the tested dependencies from requirements.txt. The [Qwen3-0.6B base model](https://huggingface.co/Qwen/Qwen3-0.6B) is downloaded separately on first load; this repository contains only the adapter and decision head.
 
 
 
 
 
 
 
 
53
 
54
  from inference import MiniJev
55
 
56
- model = MiniJev.load(".")
57
  result = model.predict(
58
  state={"user_goal": "Find tomorrow's weather forecast for Paris."},
59
  question="Given the current state and available options,\nwhich option should be selected?",
@@ -70,6 +80,15 @@ A CUDA GPU with BF16 support is required for this 4-bit package. Install the tes
70
  print(result["selected_id"])
71
  print(result["options"])
72
 
 
 
 
 
 
 
 
 
 
73
  Option IDs are bookkeeping and are excluded from model text. The model scores each supplied option and normalizes probabilities over the finite set. Input branches are limited to 8,192 tokens.
74
 
75
  ## Data attribution
 
2
  license: apache-2.0
3
  base_model: Qwen/Qwen3-0.6B
4
  library_name: custom
5
+ datasets:
6
+ - samatv256/jev-decisions-v1
7
  tags:
8
  - mini-jev
9
  - decision-model
 
51
 
52
  ## Use
53
 
54
+ A CUDA GPU with BF16 support is required for this 4-bit package. Install the tested dependencies from requirements.txt. The [Qwen3-0.6B base model](https://huggingface.co/Qwen/Qwen3-0.6B) is downloaded separately on first load; the current checkpoint consists of the adapter and decision-head files described above.
55
+
56
+ Fetch the inference module and dependency list into a working directory first:
57
+
58
+ ```bash
59
+ python -m pip install "huggingface_hub==1.33.0"
60
+ hf download samatv256/mini-Jev inference.py requirements.txt --local-dir .
61
+ python -m pip install -r requirements.txt
62
+ ```
63
 
64
  from inference import MiniJev
65
 
66
+ model = MiniJev.load() # Defaults to samatv256/mini-Jev using the HF cache.
67
  result = model.predict(
68
  state={"user_goal": "Find tomorrow's weather forecast for Paris."},
69
  question="Given the current state and available options,\nwhich option should be selected?",
 
80
  print(result["selected_id"])
81
  print(result["options"])
82
 
83
+ To pin a release, use `MiniJev.load("samatv256/mini-Jev", revision="step-010626")`.
84
+ Existing local directories and `Path` arguments remain supported, for example
85
+ `MiniJev.load(".")` after downloading the release files. Existing local paths take
86
+ precedence over repo IDs. `revision` applies only to Hub loading. Set
87
+ `HF_HUB_OFFLINE=1` to use cached files offline; both the release and its pinned Qwen
88
+ base/tokenizer must already be cached. Hub loading fetches only `config.json`,
89
+ `decision_head.safetensors`, `adapter/adapter_config.json`, and
90
+ `adapter/adapter_model.safetensors` from one revision, excluding legacy root weights.
91
+
92
  Option IDs are bookkeeping and are excluded from model text. The model scores each supplied option and normalizes probabilities over the finite set. Input branches are limited to 8,192 tokens.
93
 
94
  ## Data attribution
inference.py CHANGED
@@ -7,6 +7,8 @@ from pathlib import Path
7
  from typing import Any
8
 
9
  import torch
 
 
10
  from peft import PeftModel, prepare_model_for_kbit_training
11
  from safetensors.torch import load_file
12
  from torch import nn
@@ -215,7 +217,37 @@ class MiniJev:
215
  self.max_tokens = int(config["max_tokens"])
216
 
217
  @classmethod
218
- def load(cls, release_dir: str | Path, device: str = "cuda") -> MiniJev:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  return cls(release_dir, device)
220
 
221
  def predict(
 
7
  from typing import Any
8
 
9
  import torch
10
+ from huggingface_hub import snapshot_download
11
+ from huggingface_hub.utils import HFValidationError, validate_repo_id
12
  from peft import PeftModel, prepare_model_for_kbit_training
13
  from safetensors.torch import load_file
14
  from torch import nn
 
217
  self.max_tokens = int(config["max_tokens"])
218
 
219
  @classmethod
220
+ def load(
221
+ cls,
222
+ release_dir: str | Path = "samatv256/mini-Jev",
223
+ device: str = "cuda",
224
+ *,
225
+ revision: str | None = None,
226
+ ) -> MiniJev:
227
+ """Load a local release or a cached Hub namespace/repo at a revision.
228
+
229
+ Existing local paths and Path arguments always remain local. Revision
230
+ applies only to Hub releases. HF_HUB_OFFLINE uses the normal Hub cache.
231
+ """
232
+ if isinstance(release_dir, str) and "/" in release_dir and not Path(release_dir).exists():
233
+ try:
234
+ validate_repo_id(release_dir)
235
+ except HFValidationError:
236
+ pass # Explicit or invalid local paths retain constructor behavior.
237
+ else:
238
+ files = [
239
+ "config.json",
240
+ "decision_head.safetensors",
241
+ "adapter/adapter_config.json",
242
+ "adapter/adapter_model.safetensors",
243
+ ]
244
+ root = Path(snapshot_download(
245
+ repo_id=release_dir, revision=revision, allow_patterns=files,
246
+ ))
247
+ missing = [name for name in files if not (root / name).is_file()]
248
+ if missing:
249
+ raise FileNotFoundError("missing required release files: " + ", ".join(missing))
250
+ return cls(root, device)
251
  return cls(release_dir, device)
252
 
253
  def predict(
requirements.txt CHANGED
@@ -4,3 +4,4 @@ peft==0.21.0
4
  bitsandbytes==0.50.2
5
  safetensors==0.8.0
6
  accelerate==1.15.0
 
 
4
  bitsandbytes==0.50.2
5
  safetensors==0.8.0
6
  accelerate==1.15.0
7
+ huggingface_hub==1.33.0
tests/test_release_loading.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Release-location tests; model construction is replaced to avoid GPU work."""
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+ from huggingface_hub import constants
9
+ from huggingface_hub.errors import LocalEntryNotFoundError
10
+
11
+ import inference
12
+
13
+ FILES = (
14
+ "config.json",
15
+ "decision_head.safetensors",
16
+ "adapter/adapter_config.json",
17
+ "adapter/adapter_model.safetensors",
18
+ )
19
+
20
+
21
+ class ProbeMiniJev(inference.MiniJev):
22
+ def __init__(self, release_dir, device="cuda"):
23
+ self.release_dir = release_dir
24
+ self.device = device
25
+ self.config = json.loads((Path(release_dir) / "config.json").read_text())
26
+
27
+
28
+ @pytest.fixture
29
+ def release(tmp_path):
30
+ for name in FILES:
31
+ path = tmp_path / name
32
+ path.parent.mkdir(parents=True, exist_ok=True)
33
+ path.write_text('{"marker": "resolved"}' if name.endswith('.json') else 'weights')
34
+ return tmp_path
35
+
36
+
37
+ @pytest.fixture
38
+ def hub(monkeypatch, release):
39
+ calls = []
40
+
41
+ def download(**kwargs):
42
+ calls.append(kwargs)
43
+ return str(release)
44
+
45
+ monkeypatch.setattr(inference, "snapshot_download", download, raising=False)
46
+ return calls
47
+
48
+
49
+ def test_local_string_takes_precedence_over_repo_id(monkeypatch, tmp_path, release, hub):
50
+ local = tmp_path / "example" / "model"
51
+ local.mkdir(parents=True)
52
+ (local / "config.json").write_text('{"marker": "local"}')
53
+ monkeypatch.chdir(tmp_path)
54
+ model = ProbeMiniJev.load("example/model", "cuda:1")
55
+ assert model.config["marker"] == "local"
56
+ assert model.release_dir == "example/model"
57
+ assert model.device == "cuda:1"
58
+ assert hub == []
59
+
60
+
61
+ def test_path_remains_local(release, hub):
62
+ model = ProbeMiniJev.load(release)
63
+ assert model.release_dir is release
64
+ assert model.config["marker"] == "resolved"
65
+ assert hub == []
66
+
67
+
68
+ @pytest.mark.parametrize("argument", [None, "samatv256/mini-Jev", "example/model"])
69
+ def test_hub_default_and_explicit_repo_use_exact_allowlist(argument, release, hub):
70
+ model = ProbeMiniJev.load() if argument is None else ProbeMiniJev.load(argument)
71
+ assert model.config["marker"] == "resolved"
72
+ assert Path(model.release_dir) == release
73
+ assert len(hub) == 1
74
+ assert hub[0] == {
75
+ "repo_id": argument or "samatv256/mini-Jev",
76
+ "revision": None,
77
+ "allow_patterns": list(FILES),
78
+ }
79
+
80
+
81
+ @pytest.mark.parametrize("revision", ["step-010626", "a" * 40])
82
+ def test_revision_is_resolved_in_one_snapshot(revision, release, hub):
83
+ model = ProbeMiniJev.load("example/model", revision=revision)
84
+ assert model.config["marker"] == "resolved"
85
+ assert len(hub) == 1
86
+ assert hub[0]["revision"] == revision
87
+
88
+
89
+ @pytest.mark.parametrize("missing", FILES)
90
+ def test_incomplete_hub_snapshot_fails_before_constructor(missing, release, hub):
91
+ (release / missing).unlink()
92
+ with pytest.raises(FileNotFoundError, match="missing required release files:.*" + re.escape(missing)):
93
+ ProbeMiniJev.load("example/model")
94
+
95
+
96
+ @pytest.mark.parametrize("argument", ["./missing/model", "../missing/model", "/missing/model", "missing", Path("missing/model")])
97
+ def test_explicit_missing_local_paths_do_not_download(argument, hub):
98
+ with pytest.raises(FileNotFoundError):
99
+ ProbeMiniJev.load(argument)
100
+ assert hub == []
101
+
102
+
103
+ def test_local_revision_does_not_change_loading(release, hub):
104
+ model = ProbeMiniJev.load(str(release), revision="step-010626")
105
+ assert model.config["marker"] == "resolved"
106
+ assert hub == []
107
+
108
+
109
+ def test_offline_cache_miss_is_propagated(monkeypatch):
110
+ def download(**kwargs):
111
+ raise LocalEntryNotFoundError("no cached release")
112
+
113
+ monkeypatch.setattr(inference, "snapshot_download", download, raising=False)
114
+ with pytest.raises(LocalEntryNotFoundError, match="no cached release"):
115
+ ProbeMiniJev.load("example/model")
116
+
117
+
118
+ @pytest.mark.parametrize("revision", [None, "a" * 40])
119
+ def test_real_hub_offline_cache_hit(monkeypatch, tmp_path, release, revision):
120
+ cache = tmp_path / "cache"
121
+ storage = cache / "models--example--model"
122
+ snapshot = storage / "snapshots" / ("a" * 40)
123
+ entries = {}
124
+ for name in FILES:
125
+ data = (release / name).read_bytes()
126
+ path = snapshot / name
127
+ path.parent.mkdir(parents=True, exist_ok=True)
128
+ path.write_bytes(data)
129
+ entries[name] = {"size": len(data), "blob_id": "b" * 40}
130
+ (storage / "refs").mkdir()
131
+ (storage / "refs" / "main").write_text("a" * 40)
132
+ (storage / "trees").mkdir()
133
+ (storage / "trees" / (("a" * 40) + ".json")).write_text(
134
+ json.dumps({"format_version": 1, "files": entries})
135
+ )
136
+ monkeypatch.setattr(constants, "HF_HUB_CACHE", str(cache))
137
+ monkeypatch.setattr(constants, "HF_HUB_OFFLINE", True)
138
+ model = ProbeMiniJev.load("example/model", revision=revision)
139
+ assert Path(model.release_dir) == snapshot
140
+ assert model.config["marker"] == "resolved"
141
+
142
+
143
+ def test_real_hub_offline_cache_miss(monkeypatch, tmp_path):
144
+ monkeypatch.setattr(constants, "HF_HUB_CACHE", str(tmp_path / "empty-cache"))
145
+ monkeypatch.setattr(constants, "HF_HUB_OFFLINE", True)
146
+ with pytest.raises(LocalEntryNotFoundError):
147
+ ProbeMiniJev.load("example/uncached")