vqlab 0.1.0__py3-none-any.whl
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.
- vqlab/__init__.py +79 -0
- vqlab/_layout.py +225 -0
- vqlab/agents/CONTEXT.md +50 -0
- vqlab/agents/__init__.py +1 -0
- vqlab/agents/_fetch.py +46 -0
- vqlab/agents/bug_report.py +107 -0
- vqlab/agents/config_cmd.py +165 -0
- vqlab/agents/doctor.py +234 -0
- vqlab/agents/downloads.py +255 -0
- vqlab/agents/first_run.py +77 -0
- vqlab/agents/gui.py +511 -0
- vqlab/agents/gui_static/index.html +1605 -0
- vqlab/agents/jobs.py +257 -0
- vqlab/agents/mcp/__init__.py +15 -0
- vqlab/agents/mcp/common.py +8 -0
- vqlab/agents/mcp/server.py +290 -0
- vqlab/agents/mcp/tools_findings.py +32 -0
- vqlab/agents/mcp/tools_runs.py +44 -0
- vqlab/agents/mcp/tools_storage.py +38 -0
- vqlab/agents/model_scan.py +271 -0
- vqlab/agents/onboard.py +339 -0
- vqlab/agents/scratch.py +526 -0
- vqlab/agents/settings.py +242 -0
- vqlab/agents/teachers.py +213 -0
- vqlab/agents/updates.py +76 -0
- vqlab/api.py +605 -0
- vqlab/bench/CONTEXT.md +43 -0
- vqlab/bench/__init__.py +1 -0
- vqlab/bench/active_bytes.py +213 -0
- vqlab/bench/box_quiet.py +70 -0
- vqlab/bench/decode_ladder.py +663 -0
- vqlab/bench/decode_timeline.py +447 -0
- vqlab/bench/gdn_chunk_truth.py +162 -0
- vqlab/bench/gpu_capture.py +99 -0
- vqlab/bench/hc_micro.py +159 -0
- vqlab/bench/host_attrib.py +116 -0
- vqlab/bench/kernel_coverage.py +97 -0
- vqlab/bench/lut_bench.py +252 -0
- vqlab/bench/mtp_accept.py +172 -0
- vqlab/bench/mtp_arms.py +59 -0
- vqlab/bench/mtp_bench.py +71 -0
- vqlab/bench/power.py +23 -0
- vqlab/bench/prefill_bench.py +194 -0
- vqlab/bench/prefill_timeline.py +159 -0
- vqlab/bench/serve_timeline.py +174 -0
- vqlab/bench/speed_pair.py +124 -0
- vqlab/bench/speed_pair_knurlogic.py +213 -0
- vqlab/bench/stage_bandwidth.py +249 -0
- vqlab/bench/timeline_common.py +31 -0
- vqlab/build/CONTEXT.md +41 -0
- vqlab/build/__init__.py +1 -0
- vqlab/build/build_dense.py +331 -0
- vqlab/build/graft_extras.py +165 -0
- vqlab/build/graft_vision.py +322 -0
- vqlab/build/loo_bands.py +71 -0
- vqlab/build/minibase.py +47 -0
- vqlab/build/mix.py +161 -0
- vqlab/build/mtp_extract.py +132 -0
- vqlab/build/mtp_graft.py +165 -0
- vqlab/build/mtp_head_ds4.py +143 -0
- vqlab/build/mtp_pack.py +140 -0
- vqlab/build/pack.py +288 -0
- vqlab/build/pack_dense.py +128 -0
- vqlab/build/pack_ple.py +112 -0
- vqlab/build/ple_swap.py +121 -0
- vqlab/build/reskeleton.py +181 -0
- vqlab/build/sanitize_stream.py +157 -0
- vqlab/build/slice_teacher.py +241 -0
- vqlab/build/splice_ple.py +120 -0
- vqlab/build/stream_convert.py +330 -0
- vqlab/build/teacher_prep.py +85 -0
- vqlab/build/unpack_dense.py +136 -0
- vqlab/bundle/CONTEXT.md +22 -0
- vqlab/bundle/__init__.py +1 -0
- vqlab/bundle/add_model_file.py +333 -0
- vqlab/bundle/patch_arch.py +56 -0
- vqlab/bundle/rebundle_dense.py +80 -0
- vqlab/bundle/vision_layout.py +142 -0
- vqlab/cli.py +223 -0
- vqlab/commands.py +490 -0
- vqlab/config.py +637 -0
- vqlab/core/CONTEXT.md +27 -0
- vqlab/core/__init__.py +1 -0
- vqlab/core/artifacts/CONTEXT.md +17 -0
- vqlab/core/artifacts/__init__.py +1 -0
- vqlab/core/artifacts/artifact.py +296 -0
- vqlab/core/artifacts/artifact_size.py +27 -0
- vqlab/core/artifacts/codepack.py +49 -0
- vqlab/core/artifacts/hub.py +65 -0
- vqlab/core/artifacts/pin_record.py +45 -0
- vqlab/core/artifacts/stamp_fields.py +40 -0
- vqlab/core/box/CONTEXT.md +15 -0
- vqlab/core/box/__init__.py +1 -0
- vqlab/core/box/arch_finder.py +104 -0
- vqlab/core/box/exo.py +26 -0
- vqlab/core/box/gpu_lease.py +109 -0
- vqlab/core/box/knurlogic_client.py +151 -0
- vqlab/core/source/CONTEXT.md +18 -0
- vqlab/core/source/__init__.py +1 -0
- vqlab/core/source/clamps.py +42 -0
- vqlab/core/source/expert_src.py +168 -0
- vqlab/core/source/fit_store.py +473 -0
- vqlab/core/source/mem_budget.py +83 -0
- vqlab/core/source/ple_stream.py +196 -0
- vqlab/core/source/teacher_profiles.py +74 -0
- vqlab/family/CONTEXT.md +30 -0
- vqlab/family/__init__.py +155 -0
- vqlab/family/arch_equivalence.json +33 -0
- vqlab/family/deepseek_v4.py +269 -0
- vqlab/family/gemma4.py +187 -0
- vqlab/family/glm5_next.py +215 -0
- vqlab/family/model_loader.py +213 -0
- vqlab/family/qwen3_5.py +159 -0
- vqlab/family/qwen4_exp.py +123 -0
- vqlab/family/registry.py +317 -0
- vqlab/family/version_stamp.py +419 -0
- vqlab/fit/CONTEXT.md +35 -0
- vqlab/fit/__init__.py +1 -0
- vqlab/fit/additive_codebooks.py +98 -0
- vqlab/fit/fit_additive.py +224 -0
- vqlab/fit/fit_dense_vq.py +257 -0
- vqlab/fit/fit_moe.py +704 -0
- vqlab/fit/fit_ple.py +189 -0
- vqlab/fit/fits.py +216 -0
- vqlab/fit/geo_build.py +664 -0
- vqlab/fit/harvest_parts.py +142 -0
- vqlab/fit/kmeans.py +290 -0
- vqlab/fit/reselect.py +526 -0
- vqlab/gate/CONTEXT.md +36 -0
- vqlab/gate/__init__.py +1 -0
- vqlab/gate/bundle_accept.py +109 -0
- vqlab/gate/check_all.py +99 -0
- vqlab/gate/check_bundle.py +391 -0
- vqlab/gate/check_comparator.py +94 -0
- vqlab/gate/check_release.py +756 -0
- vqlab/gate/kernel_accept.py +118 -0
- vqlab/gate/kernel_reference.py +23 -0
- vqlab/gate/knurlogic_link.py +103 -0
- vqlab/gate/leak_guard.py +131 -0
- vqlab/gate/mtp_smoke_head.py +99 -0
- vqlab/gate/parity.py +125 -0
- vqlab/gate/pin.py +184 -0
- vqlab/gate/smoke.py +313 -0
- vqlab/gate/spelling.py +167 -0
- vqlab/gate/tokenizer_match.py +75 -0
- vqlab/gate/verify_artifact.py +209 -0
- vqlab/gate/verify_dev_codebook.py +100 -0
- vqlab/gate/vision_smoke.py +571 -0
- vqlab/mtp/CONTEXT.md +36 -0
- vqlab/mtp/__init__.py +30 -0
- vqlab/mtp/bench.py +108 -0
- vqlab/mtp/caches.py +120 -0
- vqlab/mtp/capture.py +68 -0
- vqlab/mtp/cli_common.py +40 -0
- vqlab/mtp/graft.py +56 -0
- vqlab/mtp/loop.py +541 -0
- vqlab/mtp/mtp_head.py +219 -0
- vqlab/mtp/mtp_head_glm5.py +244 -0
- vqlab/mtp/mtp_head_qwen35.py +264 -0
- vqlab/mtp/registry.py +241 -0
- vqlab/mtp/runtime.py +75 -0
- vqlab/mtp/sampling.py +145 -0
- vqlab/ops/CONTEXT.md +41 -0
- vqlab/ops/__init__.py +20 -0
- vqlab/ops/box.py +52 -0
- vqlab/ops/docs.py +42 -0
- vqlab/ops/errors.py +21 -0
- vqlab/ops/findings.py +223 -0
- vqlab/ops/queues.py +47 -0
- vqlab/ops/roots.py +57 -0
- vqlab/ops/runs.py +208 -0
- vqlab/ops/storage.py +120 -0
- vqlab/ops/supervisor.py +106 -0
- vqlab/plan/CONTEXT.md +32 -0
- vqlab/plan/__init__.py +1 -0
- vqlab/plan/alloc_sweep.py +260 -0
- vqlab/plan/family_profile.py +401 -0
- vqlab/plan/init_sweep.py +187 -0
- vqlab/plan/layer_leverage.py +346 -0
- vqlab/plan/mtp_probe_qwen35.py +185 -0
- vqlab/plan/mtp_probe_qwen4_exp.py +234 -0
- vqlab/plan/preflight_disk.py +73 -0
- vqlab/plan/preflight_ram.py +75 -0
- vqlab/plan/price.py +167 -0
- vqlab/plan/recipe_queue.py +940 -0
- vqlab/plan/zero_groups.py +186 -0
- vqlab/queue/CONTEXT.md +53 -0
- vqlab/queue/__init__.py +5 -0
- vqlab/queue/eta.py +169 -0
- vqlab/queue/files.py +175 -0
- vqlab/queue/remote.py +187 -0
- vqlab/queue/report.py +126 -0
- vqlab/queue/reserve.py +269 -0
- vqlab/queue/runner.py +457 -0
- vqlab/queue/steps.py +322 -0
- vqlab/queue/validate.py +228 -0
- vqlab/records/CONTEXT.md +24 -0
- vqlab/records/__init__.py +1 -0
- vqlab/records/artifact_manifest.py +103 -0
- vqlab/records/provenance.py +678 -0
- vqlab/records/registry.py +267 -0
- vqlab/records/runlog.py +119 -0
- vqlab/records/scores.py +83 -0
- vqlab/records/step_verdict.py +58 -0
- vqlab/runtime/CONTEXT.md +19 -0
- vqlab/runtime/__init__.py +1 -0
- vqlab/runtime/arch_resolve.py +295 -0
- vqlab/runtime/dense_shim.py +84 -0
- vqlab/runtime/equivalent/vq_dense.ac62e4e43f4a.py.txt +513 -0
- vqlab/runtime/equivalent/vq_switch.1534dd6b77b8.py.txt +4857 -0
- vqlab/runtime/equivalent/vq_switch.40870875499d.py.txt +4453 -0
- vqlab/runtime/equivalent/vq_switch.5abf28a7f096.py.txt +4958 -0
- vqlab/runtime/equivalent/vq_switch.6254c4531ed5.py.txt +5482 -0
- vqlab/runtime/equivalent/vq_switch.6d8ab6873adf.py.txt +4862 -0
- vqlab/runtime/equivalent/vq_switch.b41c1064c82b.py.txt +4856 -0
- vqlab/runtime/equivalent/vq_switch.e6fecc09a842.py.txt +4929 -0
- vqlab/runtime/equivalent_revisions.json +61 -0
- vqlab/runtime/gated_delta_chunked.py +153 -0
- vqlab/runtime/glm5_shim.py +171 -0
- vqlab/runtime/runtime_profile.py +193 -0
- vqlab/runtime/vq_dense.py +513 -0
- vqlab/runtime/vq_pack.py +129 -0
- vqlab/runtime/vq_switch.py +5495 -0
- vqlab/score/CONTEXT.md +31 -0
- vqlab/score/__init__.py +1 -0
- vqlab/score/act_stats.py +56 -0
- vqlab/score/kernel_truth.py +444 -0
- vqlab/score/kernel_truth_moe.py +129 -0
- vqlab/score/kl_damage.py +345 -0
- vqlab/score/kl_ladder.py +314 -0
- vqlab/score/kl_pair.py +133 -0
- vqlab/score/ppl_resident.py +80 -0
- vqlab/score/referee/referee_corpus.txt +350 -0
- vqlab/score/referee/referee_corpus_code_public.txt +1692 -0
- vqlab/score/referee/referee_corpus_code_public.txt.manifest.json +37 -0
- vqlab/score/referee/referee_corpus_literary.manifest.json +86 -0
- vqlab/score/referee/referee_corpus_literary.txt +22126 -0
- vqlab/score/referee/score_streaming.py +366 -0
- vqlab/score/runtime_equiv.py +179 -0
- vqlab/score/score_tasks_streaming.py +615 -0
- vqlab/score/stream_score.py +700 -0
- vqlab/selftest/CONTEXT.md +45 -0
- vqlab/selftest/__init__.py +141 -0
- vqlab/selftest/__main__.py +6 -0
- vqlab/selftest/build_checks.py +36 -0
- vqlab/selftest/family_checks.py +73 -0
- vqlab/selftest/fit_checks.py +121 -0
- vqlab/selftest/gate_checks.py +128 -0
- vqlab/selftest/kernel_checks.py +43 -0
- vqlab/selftest/layout_checks.py +90 -0
- vqlab/selftest/plan_checks.py +57 -0
- vqlab/selftest/queue_checks.py +106 -0
- vqlab/selftest/records_checks.py +171 -0
- vqlab/selftest/reselect_fixture.py +102 -0
- vqlab/selftest/skipzero_checks.py +32 -0
- vqlab/selftest/synthetic_model.py +136 -0
- vqlab/selftest/sz_bitexact_cpu.py +164 -0
- vqlab/selftest/sz_check_fixture.py +93 -0
- vqlab/ship/CONTEXT.md +27 -0
- vqlab/ship/__init__.py +1 -0
- vqlab/ship/card.py +338 -0
- vqlab/ship/card_checks.py +100 -0
- vqlab/ship/card_tables.py +115 -0
- vqlab/ship/mtp_run.py +55 -0
- vqlab/ship/publish.py +220 -0
- vqlab/ship/release_prep.py +139 -0
- vqlab/ship/serve.py +268 -0
- vqlab/ship/size.py +75 -0
- vqlab/skipzero/CONTEXT.md +88 -0
- vqlab/skipzero/__init__.py +2 -0
- vqlab/skipzero/skipzero_load.py +141 -0
- vqlab/skipzero/sz_bitexact.py +593 -0
- vqlab/skipzero/sz_check.py +162 -0
- vqlab/skipzero/sz_pack.py +355 -0
- vqlab/skipzero/sz_resident.py +756 -0
- vqlab/skipzero/sz_resident_build.py +175 -0
- vqlab/util/CONTEXT.md +22 -0
- vqlab/util/__init__.py +10 -0
- vqlab/util/clock.py +21 -0
- vqlab/util/errors.py +12 -0
- vqlab/util/fs.py +106 -0
- vqlab/util/geo.py +7 -0
- vqlab/util/hashing.py +73 -0
- vqlab/util/log.py +24 -0
- vqlab-0.1.0.dist-info/METADATA +346 -0
- vqlab-0.1.0.dist-info/RECORD +290 -0
- vqlab-0.1.0.dist-info/WHEEL +5 -0
- vqlab-0.1.0.dist-info/entry_points.txt +2 -0
- vqlab-0.1.0.dist-info/licenses/LICENSE +661 -0
- vqlab-0.1.0.dist-info/top_level.txt +1 -0
vqlab/__init__.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""VQLab: size-targeted vector-quantized builds of large models on Apple
|
|
2
|
+
Silicon (MLX), and MTP speculative decoding over stock mlx-lm.
|
|
3
|
+
|
|
4
|
+
import vqlab
|
|
5
|
+
vqlab.use_knurlogic_arch() # deepseek_v4 / qwen4_exp / Knurlogic's edits
|
|
6
|
+
from mlx_lm import load
|
|
7
|
+
from vqlab import load_mtp_head, mtp_generate
|
|
8
|
+
|
|
9
|
+
model, tok = load(path, trust_remote_code=True)
|
|
10
|
+
head, _ = load_mtp_head(model, model_path=path)
|
|
11
|
+
print(mtp_generate(model, tok, "Explain VQ.", head, temp=0.7))
|
|
12
|
+
|
|
13
|
+
`import vqlab` changes nothing outside vqlab: the only bare import names
|
|
14
|
+
it answers for are the runtime stems (vq_switch, vq_dense, ...), aliases of
|
|
15
|
+
vqlab.runtime.<stem>. `use_knurlogic_arch()` is the
|
|
16
|
+
opt-in: it puts a finder on sys.meta_path so `mlx_lm.models.<name>` resolves
|
|
17
|
+
to Knurlogic's vendored architectures (needed to load deepseek_v4 and
|
|
18
|
+
qwen4_exp, which stock mlx-lm lacks). It is idempotent, and
|
|
19
|
+
VQLAB_VENDORED_ARCH=0 turns it off. Every `vqlab` CLI command calls it.
|
|
20
|
+
|
|
21
|
+
See README.md and METHODOLOGY.md.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
from . import _layout # noqa: F401 stage folders + pre-split import names
|
|
27
|
+
from .core.box import arch_finder as _arch
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def use_knurlogic_arch() -> bool:
|
|
31
|
+
"""Serve Knurlogic's architectures (deepseek_v4, qwen4_exp, and its
|
|
32
|
+
qwen3_5 / gemma4 / glm5_next) as mlx_lm.models.<name>. Call it before
|
|
33
|
+
mlx_lm.load. Idempotent; returns False when VQLAB_VENDORED_ARCH=0 or
|
|
34
|
+
knurlogic is not installed (stock mlx-lm then serves every name)."""
|
|
35
|
+
return _arch.install_arch_finder()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _main_is_vqlab() -> bool:
|
|
39
|
+
"""True when the running program is one of vqlab's own scripts
|
|
40
|
+
(`python -m vqlab.<stage>.<tool>` or `python <pkg>/<stage>/<tool>.py`):
|
|
41
|
+
those are vqlab's entry points, and they load models as the CLI does."""
|
|
42
|
+
import os
|
|
43
|
+
import sys
|
|
44
|
+
argv = list(getattr(sys, "orig_argv", None) or [])
|
|
45
|
+
for i, a in enumerate(argv[1:], 1):
|
|
46
|
+
if a == "-m":
|
|
47
|
+
nxt = argv[i + 1] if i + 1 < len(argv) else ""
|
|
48
|
+
return nxt == "vqlab" or nxt.startswith("vqlab.")
|
|
49
|
+
if a.startswith("-m"):
|
|
50
|
+
return a[2:] == "vqlab" or a[2:].startswith("vqlab.")
|
|
51
|
+
if a.startswith("-"):
|
|
52
|
+
if a in ("-c",):
|
|
53
|
+
return False
|
|
54
|
+
continue
|
|
55
|
+
pkg = os.path.dirname(os.path.realpath(__file__))
|
|
56
|
+
return os.path.realpath(a).startswith(pkg + os.sep)
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if _main_is_vqlab():
|
|
61
|
+
use_knurlogic_arch()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# The MTP surface loads mlx, so it resolves on first use (PEP 562): a plain
|
|
65
|
+
# `import vqlab` -- and `import vqlab.api`, the stable library/MCP surface --
|
|
66
|
+
# stays free of mlx. `from vqlab import load_mtp_head` works as before.
|
|
67
|
+
_MTP_NAMES = ("FAMILIES", "FamilySpec", "MTPResponse", "load_mtp_head",
|
|
68
|
+
"mtp_generate", "mtp_stream_generate", "register")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def __getattr__(name):
|
|
72
|
+
if name in _MTP_NAMES:
|
|
73
|
+
from . import mtp
|
|
74
|
+
return getattr(mtp, name)
|
|
75
|
+
raise AttributeError(f"module 'vqlab' has no attribute {name!r}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = ["use_knurlogic_arch", "FAMILIES", "FamilySpec", "MTPResponse", "load_mtp_head",
|
|
79
|
+
"mtp_generate", "mtp_stream_generate", "register", "__version__"]
|
vqlab/_layout.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Names shipped text depends on, and the locators that find a module as a FILE.
|
|
2
|
+
|
|
3
|
+
Every module has ONE canonical dotted name: its path under src/vqlab
|
|
4
|
+
(`vqlab.runtime.vq_switch`, `vqlab.fit.fit_moe`). PERMANENT lists the only
|
|
5
|
+
other spellings, kept forever because shipped text names them:
|
|
6
|
+
|
|
7
|
+
* every runtime/*.py stem, as `vqlab.<stem>` AND bare `<stem>`: the
|
|
8
|
+
SHIPPED runtime text (vq_dense.py's _resolve_kernel, dense_shim.py) and
|
|
9
|
+
published model.py files name them, and inside a bundle the files sit
|
|
10
|
+
flat beside model.py. Must stay byte-identical, so the names must stay;
|
|
11
|
+
* `vqlab.mtp_head*`, `vq_397b_codes` and `pack_artifact`: archived queue
|
|
12
|
+
files and ledgers name them.
|
|
13
|
+
|
|
14
|
+
Whatever name is used, the result is the SAME module object (_Alias): two
|
|
15
|
+
copies of vq_switch would mean two kernel caches and two sets of flag
|
|
16
|
+
globals.
|
|
17
|
+
|
|
18
|
+
Locators, for code that reads a module as a FILE rather than importing it:
|
|
19
|
+
|
|
20
|
+
find("smoke.py") -> path of any tool (and vq_397b_codes.py)
|
|
21
|
+
runtime_file("vq_switch.py") -> the repo runtime a bundle is spliced from
|
|
22
|
+
corpus("prose") -> a house referee corpus
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import importlib
|
|
27
|
+
import importlib.abc
|
|
28
|
+
import importlib.machinery
|
|
29
|
+
import importlib.util
|
|
30
|
+
import pathlib
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
PKG = pathlib.Path(__file__).resolve().parent
|
|
34
|
+
SRC = PKG.parent
|
|
35
|
+
|
|
36
|
+
PERMANENT = {
|
|
37
|
+
'arch_resolve': 'vqlab.runtime.arch_resolve',
|
|
38
|
+
'dense_shim': 'vqlab.runtime.dense_shim',
|
|
39
|
+
'gated_delta_chunked': 'vqlab.runtime.gated_delta_chunked',
|
|
40
|
+
'glm5_shim': 'vqlab.runtime.glm5_shim',
|
|
41
|
+
'runtime_profile': 'vqlab.runtime.runtime_profile',
|
|
42
|
+
'vq_dense': 'vqlab.runtime.vq_dense',
|
|
43
|
+
'vq_pack': 'vqlab.runtime.vq_pack',
|
|
44
|
+
'vq_switch': 'vqlab.runtime.vq_switch',
|
|
45
|
+
'vqlab.arch_resolve': 'vqlab.runtime.arch_resolve',
|
|
46
|
+
'vqlab.build.pack_artifact': 'vqlab.build.pack',
|
|
47
|
+
'vqlab.dense_shim': 'vqlab.runtime.dense_shim',
|
|
48
|
+
'vqlab.fit.vq_397b_codes': 'vqlab.fit.fit_moe',
|
|
49
|
+
'vqlab.gated_delta_chunked': 'vqlab.runtime.gated_delta_chunked',
|
|
50
|
+
'vqlab.glm5_shim': 'vqlab.runtime.glm5_shim',
|
|
51
|
+
'vqlab.mtp_head': 'vqlab.mtp.mtp_head',
|
|
52
|
+
'vqlab.mtp_head_ds4': 'vqlab.build.mtp_head_ds4',
|
|
53
|
+
'vqlab.mtp_head_glm5': 'vqlab.mtp.mtp_head_glm5',
|
|
54
|
+
'vqlab.mtp_head_qwen35': 'vqlab.mtp.mtp_head_qwen35',
|
|
55
|
+
'vqlab.pack_artifact': 'vqlab.build.pack',
|
|
56
|
+
'vqlab.runtime_profile': 'vqlab.runtime.runtime_profile',
|
|
57
|
+
'vqlab.vq_397b_codes': 'vqlab.fit.fit_moe',
|
|
58
|
+
'vqlab.vq_dense': 'vqlab.runtime.vq_dense',
|
|
59
|
+
'vqlab.vq_pack': 'vqlab.runtime.vq_pack',
|
|
60
|
+
'vqlab.vq_switch': 'vqlab.runtime.vq_switch',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
assert not {"queue", "vqlab.queue"} & set(PERMANENT) # stdlib `queue` stays the stdlib's
|
|
64
|
+
|
|
65
|
+
# Old FILENAMES the PERMANENT names imply (vq_397b_codes.py -> fit_moe.py,
|
|
66
|
+
# pack_artifact.py -> pack.py): archived queue files and ledgers name them.
|
|
67
|
+
FILE_NAMES = {k.rpartition(".")[2]: v.rpartition(".")[2] for k, v in PERMANENT.items()
|
|
68
|
+
if k.rpartition(".")[2] != v.rpartition(".")[2]}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Folders find() searches, in order: a current filename needs no table.
|
|
72
|
+
_FIND_DIRS = ("", "runtime", "core", "core/artifacts", "core/source", "core/box",
|
|
73
|
+
"family", "plan", "fit", "build", "bundle",
|
|
74
|
+
"gate", "score", "bench", "records", "ship", "agents", "mtp",
|
|
75
|
+
"queue", "selftest", "skipzero", "ops", "util", "score/referee")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find(name: str) -> pathlib.Path:
|
|
79
|
+
"""Path of a module FILE by its filename or relative path. The old
|
|
80
|
+
filenames PERMANENT implies (vq_397b_codes.py) map to their module."""
|
|
81
|
+
stem, dot, ext = name.rpartition("/")[2].partition(".")
|
|
82
|
+
new = FILE_NAMES.get(stem)
|
|
83
|
+
if new and ext == "py":
|
|
84
|
+
name = name[:len(name) - len(stem + dot + ext)] + new + dot + ext
|
|
85
|
+
for d in _FIND_DIRS:
|
|
86
|
+
p = PKG / d / name
|
|
87
|
+
if p.exists():
|
|
88
|
+
return p
|
|
89
|
+
if name.startswith("referee/"): # old relative spelling
|
|
90
|
+
return find(name.split("/", 1)[1])
|
|
91
|
+
raise FileNotFoundError(f"vqlab: no module file {name!r} under {PKG}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def runtime_file(name: str) -> pathlib.Path:
|
|
95
|
+
"""The repo's copy of a shipped runtime file (vq_switch.py, vq_dense.py,
|
|
96
|
+
vq_pack.py, dense_shim.py, glm5_shim.py). This is the text bundlers
|
|
97
|
+
splice and check-bundle compares against; its flag DEFAULTS are the
|
|
98
|
+
repo's, so a caller that needs a specific profile applies it with
|
|
99
|
+
runtime_profile.apply_profile / resolve_runtime."""
|
|
100
|
+
p = PKG / "runtime" / name
|
|
101
|
+
if not p.exists():
|
|
102
|
+
raise FileNotFoundError(f"vqlab: {name} is not a runtime file ({p})")
|
|
103
|
+
return p
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
CORPORA = {"prose": "referee_corpus.txt",
|
|
107
|
+
"code": "referee_corpus_code_public.txt",
|
|
108
|
+
"lit": "referee_corpus_literary.txt"}
|
|
109
|
+
_CORPUS_ALIAS = {"wikitext": "prose", "code-public": "code", "literary": "lit"}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def corpus(name: str = "prose") -> pathlib.Path:
|
|
113
|
+
"""A house referee corpus by NAME (prose / code / lit), never by a path
|
|
114
|
+
built from __file__: a scratch script that hardcodes the repo layout
|
|
115
|
+
breaks silently when the layout moves (night 4, 2026-09-26). A path
|
|
116
|
+
argument is passed through legacy_path, so old spellings still resolve."""
|
|
117
|
+
key = _CORPUS_ALIAS.get(name, name)
|
|
118
|
+
if key in CORPORA:
|
|
119
|
+
p = PKG / "score" / "referee" / CORPORA[key]
|
|
120
|
+
else:
|
|
121
|
+
p = pathlib.Path(legacy_path(str(name)))
|
|
122
|
+
if not p.is_file():
|
|
123
|
+
raise FileNotFoundError(f"vqlab: no corpus {name!r} (have {sorted(CORPORA)})")
|
|
124
|
+
return p
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def legacy_path(p):
|
|
128
|
+
"""Map a path recorded BEFORE the stage split onto its new home.
|
|
129
|
+
|
|
130
|
+
Teacher caches store their corpus path in meta.json (absolute or
|
|
131
|
+
repo-relative) and shell chains pass corpora by path; both still name
|
|
132
|
+
src/vqlab/referee/. Existing paths are returned unchanged."""
|
|
133
|
+
s = str(p)
|
|
134
|
+
if pathlib.Path(s).exists():
|
|
135
|
+
return p
|
|
136
|
+
# "vqlab/referee/" (no src/) also covers an INSTALLED package's site-packages path
|
|
137
|
+
for old, new in (("vqlab/referee/", "vqlab/score/referee/"),):
|
|
138
|
+
if old in s:
|
|
139
|
+
cand = s.replace(old, new)
|
|
140
|
+
if pathlib.Path(cand).exists() or not pathlib.Path(s).is_absolute():
|
|
141
|
+
return type(p)(cand) if not isinstance(p, str) else cand
|
|
142
|
+
return p
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _Alias(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
|
146
|
+
"""ONE module object per module, whatever name it is imported by.
|
|
147
|
+
|
|
148
|
+
Without this, `import vq_switch` and `vqlab.runtime.vq_switch` load the
|
|
149
|
+
file twice: two kernel caches, two sets of env-flag globals, and a test
|
|
150
|
+
that reloads one name silently leaves the other untouched.
|
|
151
|
+
|
|
152
|
+
It answers ONLY for the PERMANENT names: `vqlab.*` spellings (our
|
|
153
|
+
namespace) and the bare runtime stems a bundle's runtime text refers to
|
|
154
|
+
(vq_switch, vq_dense, ...). Every other name gets None from it, so it
|
|
155
|
+
cannot shadow another package's module.
|
|
156
|
+
|
|
157
|
+
It sits FIRST on sys.meta_path, and that is required, not habit: a
|
|
158
|
+
script directory holding a copy of vq_switch.py (sys.path[0]) would
|
|
159
|
+
otherwise give the runtime a second module object.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(self):
|
|
163
|
+
self.map = PERMANENT
|
|
164
|
+
self._fresh = set() # ids of modules create_module just imported
|
|
165
|
+
|
|
166
|
+
def _canon(self, fullname):
|
|
167
|
+
return self.map.get(fullname)
|
|
168
|
+
|
|
169
|
+
def find_spec(self, fullname, path=None, target=None):
|
|
170
|
+
new = self._canon(fullname)
|
|
171
|
+
if new is None or new == fullname:
|
|
172
|
+
return None
|
|
173
|
+
t = importlib.util.find_spec(new)
|
|
174
|
+
return importlib.util.spec_from_loader(
|
|
175
|
+
fullname, self, origin=t.origin if t else f"alias of {new}")
|
|
176
|
+
|
|
177
|
+
def create_module(self, spec):
|
|
178
|
+
mod = importlib.import_module(self._canon(spec.name))
|
|
179
|
+
sys.modules[spec.name] = mod
|
|
180
|
+
self._fresh.add(id(mod))
|
|
181
|
+
return mod
|
|
182
|
+
|
|
183
|
+
def exec_module(self, module):
|
|
184
|
+
"""First import: the canonical import already executed it. RELOAD
|
|
185
|
+
(importlib.reload by the alias name, which is how the flag tests
|
|
186
|
+
re-read env defaults) must genuinely re-execute. Either way the
|
|
187
|
+
module keeps its CANONICAL identity; the import system just stamped
|
|
188
|
+
the alias spec on it."""
|
|
189
|
+
name = module.__spec__.name
|
|
190
|
+
canon = self._canon(name) or name
|
|
191
|
+
# Ask the path finder for the FILE's spec. importlib.util.find_spec
|
|
192
|
+
# would return the module's current __spec__ -- the alias spec the
|
|
193
|
+
# import system just stamped on it -- and recurse back into us.
|
|
194
|
+
parent, _, _ = canon.rpartition(".")
|
|
195
|
+
spec = importlib.machinery.PathFinder.find_spec(
|
|
196
|
+
canon, importlib.import_module(parent).__path__)
|
|
197
|
+
if id(module) in self._fresh:
|
|
198
|
+
self._fresh.discard(id(module))
|
|
199
|
+
else:
|
|
200
|
+
spec.loader.exec_module(module)
|
|
201
|
+
module.__spec__, module.__name__, module.__loader__ = (
|
|
202
|
+
spec, spec.name, spec.loader)
|
|
203
|
+
|
|
204
|
+
# `python -m vqlab.<old name>` goes through runpy, which asks the loader
|
|
205
|
+
# for code rather than a module. Hand it the real module's code.
|
|
206
|
+
def _target(self, fullname):
|
|
207
|
+
return importlib.util.find_spec(self._canon(fullname))
|
|
208
|
+
|
|
209
|
+
def get_code(self, fullname):
|
|
210
|
+
t = self._target(fullname)
|
|
211
|
+
return t.loader.get_code(t.name)
|
|
212
|
+
|
|
213
|
+
def get_source(self, fullname):
|
|
214
|
+
t = self._target(fullname)
|
|
215
|
+
return t.loader.get_source(t.name)
|
|
216
|
+
|
|
217
|
+
def is_package(self, fullname):
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def install_import_aliases():
|
|
222
|
+
if not any(isinstance(f, _Alias) for f in sys.meta_path):
|
|
223
|
+
sys.meta_path.insert(0, _Alias()) # first: see _Alias docstring
|
|
224
|
+
|
|
225
|
+
install_import_aliases()
|
vqlab/agents/CONTEXT.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# agents/ — Agents: the lab over MCP
|
|
2
|
+
|
|
3
|
+
## Inputs
|
|
4
|
+
JSON-RPC over stdio (`mcp`). (Queue files: `queue/`, untangle step 12.)
|
|
5
|
+
|
|
6
|
+
## Process
|
|
7
|
+
`vqlab mcp`: `where_is`, `list_artifacts`, `run` (allowlisted, detached, under the GPU lease), `next_f_number` (reserve=true before a long run), `findings_append`, ...
|
|
8
|
+
|
|
9
|
+
The server is the package `mcp/` (untangle step 13), and since step 16b it is a THIN ADAPTER: each tool is a JSON schema plus an argument mapping onto `vqlab.api` (or `vqlab.ops` where the api deliberately has no call: `run` returns the launcher's raw record, `artifact_config`, `read_doc`, `queue_status`). No lab logic lives here; the implementations are in `src/vqlab/ops/` (see its CONTEXT.md). `mcp/server.py` (stdio JSON-RPC loop + the tool table: name -> fn, schema; `--supervise` delegates to `vqlab.ops.supervisor`), `mcp/common.py` (`ToolError` = `vqlab.ops.errors.LabError` = `api.ApiError`), `mcp/tools_storage.py`, `mcp/tools_runs.py`, `mcp/tools_findings.py` (the `t_*` adapters). Patch and import the defining `vqlab.ops.*` module, never an adapter.
|
|
10
|
+
|
|
11
|
+
`vqlab lab scratch reclaimable` (`scratch.py`) surveys the scratch root, or each `--root` (HDD folders too), by evidence class. `packed`: an unpacked dir whose verified pack's build record names it. `preflight`: `<preflight_dir>/<queue>` copies whose queue (state.json, read through `queue.report.live_status` / `runner_pid`) is finished with no live runner, or whose queue record is gone. `fits`: dirs whose every safetensors holds only VQ triples already filed in the fit store (fit_id + size from `fit_store.read_index`, then tensor bytes against the filed copy; `--full` re-hashes the filed copy). `caches`: unregistered KL caches sha256-identical to a registered one (`registry.teacher_caches`); a registered cache is always KEPT. `campaigns`: `F###-name` folders whose finding is in the log, ARCHIVE candidates only. Each item carries its evidence (`--json`: `cls`, `evidence`, `detail`); every other top-level dir is reported KEPT with the reason; anything a symlink (roots + models) resolves into is kept. It prints the rm lines and never deletes. `reclaimable()` is the `packed` class alone (the GUI's).
|
|
12
|
+
|
|
13
|
+
`vqlab queue run <file>` (code in `queue/`, see its CONTEXT.md): a multi-step campaign (a night of scoring) run from a git worktree PINNED at one commit, holding the same GPU lease as `run`, retrying resumable builds, refusing unsmoked pins, and failing a step on a nonzero exit, a traceback, or missing output. `--preflight` runs every step's small real version first; `--detach` survives the shell. `queue status` (and MCP `queue_status`) prints an ETA from the running step's own progress lines (fit-moe `[i/N] shard (Ts)`, kl-ladder cells) or past passed runs of the same step, else "ETA unknown". `--on BOX` first checks over ssh that every path the queue names exists AND reads on that box (an SMB mount can list while reads fail EIO), and refuses with the fix ("remount <volume> on <box>").
|
|
14
|
+
|
|
15
|
+
`vqlab reserve` (`queue/reserve.py`) `BOX|here --for WHO --until +3h|HH:MM [--note]`, `--list`, `--release`: intent, not a lock. Stored in `<scratch>/vqlab-reservations.json` (the shared SSD every box mounts; `$VQLAB_RESERVATIONS` overrides; falls back to `~/.vqlab/reservations.json`, local-only, when scratch is unreachable). Expired ones are ignored. MCP `gpu_state` lists every box's; `queue run` / `queue run --on BOX` refuse a box reserved for someone other than `$VQLAB_WHO` (else login) unless `--override-reservation`, which state.json records.
|
|
16
|
+
|
|
17
|
+
`[boxes.NAME] teachers = "<path>"`: that box's local teacher copies. `config.teachers()` returns it when running ON that box (`$VQLAB_BOX`, which `--on` sets, else hostname), and `vqlab <cmd>` expands a `<teachers>/X` argument through it, so a queue step naming `<teachers>/Model` reads the local copy there.
|
|
18
|
+
|
|
19
|
+
`vqlab lab config` / `lab config set KEY PATH` / `lab config init` and the GUI's Settings view share ONE implementation, `agents/settings.py` (`locations`, `validate`, `set_value`, `campaign_sizes`): validate (absolute, a directory or creatable with `--create`, writable, free space shown), then rewrite only that key's line in `[paths]`. Every location defaults under the lab folder `lab` (`$VQLAB_HOME`, `[paths] lab`, else `~/VQLab`): `<lab>/models`, `scratch`, `fits`, `records`, and `teachers` (bf16 teachers) defaults to the models folder; with no `lab` set, data already under `~/.vqlab` or `~/.local/share/vqlab` keeps resolving there. `usage()` (GUI `/api/storage/usage`) groups the locations by volume with what each holds. `records` (`$VQLAB_RECORDS`, else `<lab>/records`) holds user-written state: `FINDINGS-LOG.md` (`$VQLAB_FINDINGS_LOG` still overrides), `registry/`, `families/` (`$VQLAB_FAMILIES_DIR`), `research/`, `queues/` (`$VQLAB_QUEUE_DIR`). Accessors: `config.records()`, `findings_log()`, `registry_dir()`, `families_dir()`, `research_dir()`, `queues_dir()`. A checkout that wants the old in-repo `lab/` sets `records` to it.
|
|
20
|
+
|
|
21
|
+
## Outputs
|
|
22
|
+
Detached runs under the MCP runs dir, FINDINGS-LOG entries; queues under `<records>/queues/<stamp>-<name>/` (state.json, steps/NN-name/{cmd,stdout,stderr,verdict.json}).
|
|
23
|
+
|
|
24
|
+
## Rules that bite
|
|
25
|
+
- Use `where_is` before claiming anything is missing.
|
|
26
|
+
- `run` launches through `python -m vqlab.cli`, so every agent job is in the run log.
|
|
27
|
+
- A queue step never imports the live checkout: commit mid-run freely. Uncommitted src/ changes are NOT in the queue (it refuses unless --allow-dirty).
|
|
28
|
+
- Every queue step needs a `preflight` block; run `--preflight` before any multi-hour queue.
|
|
29
|
+
- `onboard.py` (`vqlab plan onboard`) lives here, not in plan/: it drives the MCP server's `t_run`/`t_status`. The GPU lease file logic is `core/box/gpu_lease.py`.
|
|
30
|
+
|
|
31
|
+
## Files
|
|
32
|
+
- `config_cmd.py`: `vqlab lab config`: show, set or initialise storage locations (over settings.py)
|
|
33
|
+
- `doctor.py`: `vqlab lab doctor`: which interpreter, packages, storage and credentials a run would use, and whether a newer vqlab is out
|
|
34
|
+
- `updates.py`: is a newer vqlab on PyPI (one request a day at most, cached in records; VQLAB_OFFLINE skips it); doctor and the GUI read it
|
|
35
|
+
- `bug_report.py`: `vqlab lab report-bug`: a prefilled GitHub issue (versions, chip, last failed command; portable paths, no argv); prints the link, never sends
|
|
36
|
+
- `model_scan.py`: `vqlab lab scan`: models already on this machine (vqlab's own roots, the Hugging Face cache, LM Studio, exo, Ollama, `[scan] extra`), tagged original / quantized / incomplete / unreadable with their vqlab family, into `<records>/registry/found.jsonl`; reads config.json and file lists only. The GUI rescans at launch and on Rescan; first run scans once
|
|
37
|
+
- `first_run.py`: on a new install (no config, no location env, no older data) the first `vqlab` command creates `~/VQLab/{models,scratch,fits,records}` (bf16 teachers default to models/) and a config naming it, and says so once; `$VQLAB_NO_SETUP=1` skips it
|
|
38
|
+
- `downloads.py`: the GUI's Hub downloads (`/api/downloads`, POST `/start` `/stop`): one fetch process per repo (`_fetch.py`: `snapshot_download` into `<teachers>/<org>--<name>`), progress from bytes on disk, a model scan when done; never deletes
|
|
39
|
+
- `gui.py`: `vqlab lab gui`: serves the VQ Lab GUI (Library · Recipe · Jobs · Downloads · Storage: gui_static/index.html, built from gui_static/bench/bench.html) and the JSON it reads
|
|
40
|
+
- `jobs.py`: the GUI's Start / Stop / Resume of `vqlab queue` runs: a confirmed, single-use plan token, then the queue CLI (never a step itself)
|
|
41
|
+
- `teachers.py`: the GUI's teachers (`/api/teachers`): profiled teachers with exact per-geometry sizes, stored fits and whether a build chain exists, plus scanned originals that only need `family-profile` (POST `/api/teachers/profile`)
|
|
42
|
+
- `gui_static/bench/make_jobs_snapshot.py`: regenerates the GUI's embedded jobs snapshot (bench.html / index.html)
|
|
43
|
+
- `mcp/server.py`: `vqlab lab mcp`: the stdio JSON-RPC loop and the tool table (name -> fn, schema)
|
|
44
|
+
- `mcp/common.py`: `ToolError` (= `vqlab.ops.errors.LabError`)
|
|
45
|
+
- `mcp/tools_storage.py`: adapters: where_is, list_artifacts, artifact_config, read_doc, disk_free
|
|
46
|
+
- `mcp/tools_runs.py`: adapters: run, status, stop, list_runs, gpu_state, queue_status
|
|
47
|
+
- `mcp/tools_findings.py`: adapters: next_f_number (reserve / release), findings_tail, findings_append
|
|
48
|
+
- `onboard.py`: `vqlab plan onboard`: sequence a new teacher through docs/ONBOARDING.md
|
|
49
|
+
- `scratch.py`: `vqlab lab scratch`: what on scratch (or any root) a human could delete or archive, by evidence class, and why
|
|
50
|
+
- `settings.py`: storage settings: read, validate and write vqlab's locations (CLI and GUI share it)
|
vqlab/agents/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""vqlab agents stage: see CONTEXT.md in this folder."""
|
vqlab/agents/_fetch.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""One GUI download's own process (agents/downloads.py starts it):
|
|
2
|
+
|
|
3
|
+
python -m vqlab.agents._fetch REPO DEST LOCKFILE RCFILE PATTERN...
|
|
4
|
+
|
|
5
|
+
`huggingface_hub.snapshot_download` of REPO into DEST, only the files the
|
|
6
|
+
PATTERNs allow. The process holds a flock on DEST/LOCKFILE for its whole
|
|
7
|
+
life, with its pid in it: the server signals a recorded pid only while that
|
|
8
|
+
lock says it is still this fetch (a pid alone can be reused after a
|
|
9
|
+
restart). A second fetch of the same folder exits 2 and touches nothing. It
|
|
10
|
+
records how it ended in DEST/RCFILE: 0 done, 1 failed (its stderr log says
|
|
11
|
+
why). A stopped or interrupted fetch (SIGTERM, ctrl-c) records nothing.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import fcntl
|
|
16
|
+
import os
|
|
17
|
+
import pathlib
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from huggingface_hub import snapshot_download
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(argv: list[str]) -> int:
|
|
24
|
+
repo, dest, lockfile, rcfile, *allow = argv
|
|
25
|
+
d = pathlib.Path(dest)
|
|
26
|
+
lk = open(d / lockfile, "a+") # held open until exit: the lock is the liveness
|
|
27
|
+
try:
|
|
28
|
+
fcntl.flock(lk, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
29
|
+
except BlockingIOError:
|
|
30
|
+
# another fetch owns this folder: its pid and exit file stay its own
|
|
31
|
+
print(f"another fetch of {d} is running; not starting a second", file=sys.stderr)
|
|
32
|
+
return 2
|
|
33
|
+
lk.truncate(0)
|
|
34
|
+
lk.write(str(os.getpid()))
|
|
35
|
+
lk.flush()
|
|
36
|
+
try:
|
|
37
|
+
snapshot_download(repo, local_dir=d, allow_patterns=allow)
|
|
38
|
+
except Exception:
|
|
39
|
+
(d / rcfile).write_text("1")
|
|
40
|
+
raise
|
|
41
|
+
(d / rcfile).write_text("0")
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""`vqlab lab report-bug`: a GitHub issue, filled in with what a bug needs.
|
|
2
|
+
|
|
3
|
+
Builds the issue body from the versions that decide behaviour (vqlab,
|
|
4
|
+
Python, macOS, the chip and its memory, mlx, mlx-lm, knurlogic,
|
|
5
|
+
huggingface_hub) and the last command that failed, then prints the
|
|
6
|
+
new-issue link (and, with --open, opens it). Nothing is sent: the browser
|
|
7
|
+
shows the issue for the user to read, edit and submit. Every path in it is
|
|
8
|
+
written in its portable form (config.portable_text), and argv is left out:
|
|
9
|
+
it names the user's files.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import platform
|
|
15
|
+
import sys
|
|
16
|
+
import urllib.parse
|
|
17
|
+
|
|
18
|
+
from vqlab import config
|
|
19
|
+
|
|
20
|
+
PACKAGES = ("mlx", "mlx-lm", "knurlogic", "huggingface_hub")
|
|
21
|
+
MAX_URL = 7000 # GitHub refuses longer new-issue links
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def issues_url() -> str:
|
|
25
|
+
"""The project's issue tracker, from its metadata (pyproject
|
|
26
|
+
[project.urls] Issues): the installed package's, else the checkout's."""
|
|
27
|
+
from importlib.metadata import PackageNotFoundError, metadata
|
|
28
|
+
try:
|
|
29
|
+
for u in metadata("vqlab").get_all("Project-URL") or []:
|
|
30
|
+
name, _, url = u.partition(", ")
|
|
31
|
+
if name == "Issues":
|
|
32
|
+
return url
|
|
33
|
+
except PackageNotFoundError:
|
|
34
|
+
pass
|
|
35
|
+
import tomllib
|
|
36
|
+
|
|
37
|
+
from vqlab import _layout
|
|
38
|
+
pp = _layout.SRC.parent / "pyproject.toml"
|
|
39
|
+
return tomllib.loads(pp.read_text())["project"]["urls"]["Issues"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def environment() -> list[str]:
|
|
43
|
+
"""`name: version` lines for what decides behaviour; no paths."""
|
|
44
|
+
import vqlab
|
|
45
|
+
from vqlab.agents import doctor
|
|
46
|
+
lines = [f"vqlab: {vqlab.__version__}", f"python: {platform.python_version()}",
|
|
47
|
+
f"macOS: {platform.mac_ver()[0] or platform.platform()}"]
|
|
48
|
+
m = doctor.metal_info()
|
|
49
|
+
lines.append(f"chip: {m.get('device')}, {m.get('memory_gib')} GiB" if m.get("available")
|
|
50
|
+
else "chip: Metal unavailable")
|
|
51
|
+
lines += [f"{p}: {doctor.package_version(p) or 'not installed'}" for p in PACKAGES]
|
|
52
|
+
return lines
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def last_failure() -> dict | None:
|
|
56
|
+
"""The most recent logged command that ended with an error: its name,
|
|
57
|
+
exit status and error text (portable), never its arguments."""
|
|
58
|
+
from vqlab.records import runlog
|
|
59
|
+
for r in reversed(runlog.runs()):
|
|
60
|
+
rc = (r.get("end") or {}).get("rc")
|
|
61
|
+
if rc not in (None, 0):
|
|
62
|
+
return {"cmd": r.get("cmd"), "time": r.get("time"),
|
|
63
|
+
"rc": config.portable_text(str(rc))}
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def body(what: str = "") -> str:
|
|
68
|
+
"""The issue text: the user's description, then the facts."""
|
|
69
|
+
out = ["**What happened, and what did you expect?**", "", what or "<describe it here>", "",
|
|
70
|
+
"**Command and output** (paste them; check for anything private first)", "",
|
|
71
|
+
"```", "", "```", "", "**Environment**", "", "```", *environment(), "```"]
|
|
72
|
+
fail = last_failure()
|
|
73
|
+
if fail:
|
|
74
|
+
out += ["", "**Last failed command**", "", "```",
|
|
75
|
+
f"vqlab {fail['cmd']} ({fail['time']})", f"exit: {fail['rc']}", "```"]
|
|
76
|
+
return "\n".join(out) + "\n"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def new_issue_url(title: str, text: str) -> str:
|
|
80
|
+
"""GitHub's prefilled new-issue link; the body is cut to fit the limit."""
|
|
81
|
+
base = issues_url().rstrip("/") + "/new?"
|
|
82
|
+
q = {"title": title, "labels": "bug", "body": text}
|
|
83
|
+
url = base + urllib.parse.urlencode(q)
|
|
84
|
+
while len(url) > MAX_URL and len(q["body"]) > 200:
|
|
85
|
+
q["body"] = q["body"][: len(q["body"]) * 3 // 4] + "\n…(cut to fit the link)\n"
|
|
86
|
+
url = base + urllib.parse.urlencode(q)
|
|
87
|
+
return url
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main(argv=None) -> int:
|
|
91
|
+
ap = argparse.ArgumentParser(prog="vqlab lab report-bug", description=__doc__.split("\n")[0])
|
|
92
|
+
ap.add_argument("--title", default="", help="one line saying what went wrong")
|
|
93
|
+
ap.add_argument("--what", default="", help="what happened and what you expected")
|
|
94
|
+
ap.add_argument("--open", action="store_true", help="open the issue in the browser")
|
|
95
|
+
a = ap.parse_args(argv)
|
|
96
|
+
text = body(a.what)
|
|
97
|
+
url = new_issue_url(a.title or "bug: ", text)
|
|
98
|
+
print(text)
|
|
99
|
+
print(f"Open this to file it on GitHub (nothing is sent until you submit):\n{url}")
|
|
100
|
+
if a.open:
|
|
101
|
+
import webbrowser
|
|
102
|
+
webbrowser.open(url)
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
sys.exit(main())
|