robotensor-attune 0.2.1__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.
robotensor/miner.py ADDED
@@ -0,0 +1,260 @@
1
+ """`attune miner`: check a `vector_v1.1` weights file, submit it and commit it on chain.
2
+
3
+ check model.safetensors, the only file a submission holds, against the architecture the
4
+ validator serves, as the validator checks it
5
+ upload model.safetensors (and an optional README) to your Hugging Face model repository;
6
+ prints the commit sha to commit
7
+ commit write your repository, its revision and your weights' sha256 as your hotkey's
8
+ commitment on the subnet (`vector:<repo>@<commit>.<digest>`)
9
+ status your hotkey's commitment on chain, and what the Hub shows for it
10
+
11
+ Order matters for copy protection: upload to a PRIVATE repository, commit the sha on chain, then
12
+ make the repository public - at once: the validator queues a commitment as soon as it reads it and
13
+ looks the repository up on the Hub when its turn comes, and a repository it cannot see then (still
14
+ private, gone, or the Hub not answering) is dropped as missing and the next entry duels instead. Weights byte-identical to an earlier
15
+ commitment's are refused as a duplicate, and the earlier commitment keeps them, so whoever commits
16
+ first owns the weights.
17
+
18
+ Each hotkey makes one submission: once a commitment of yours is queued, anything else the hotkey
19
+ commits is refused, before its duel and after it - even if its repository is then skipped. Check the
20
+ weights (`check`) before you commit, and keep the repository up: a king whose repository is gone is
21
+ dethroned, and its share of the emission is burned.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import hashlib
28
+ import json
29
+ import os
30
+ import sys
31
+ from pathlib import Path
32
+ from typing import Any
33
+
34
+ from . import hub
35
+ from .protocol import commitment as commitment_
36
+
37
+ DEFAULT_NETWORK = "test"
38
+
39
+
40
+ def weights_digest(path: Path) -> str:
41
+ """The sha256 of the weights file, as the commitment names it."""
42
+ digest = hashlib.sha256()
43
+ with open(path, "rb") as fh:
44
+ for block in iter(lambda: fh.read(1 << 20), b""):
45
+ digest.update(block)
46
+ return digest.hexdigest()
47
+
48
+
49
+ def cmd_check(args: argparse.Namespace) -> int:
50
+ from vector_runtime import check
51
+
52
+ report = check.check_weights(
53
+ Path(args.dir) / hub.WEIGHTS_FILE if Path(args.dir).is_dir() else Path(args.dir)
54
+ )
55
+ print(
56
+ json.dumps(
57
+ {
58
+ "ok": report.ok,
59
+ "errors": list(report.errors),
60
+ "weights_sha256": report.weights_sha256,
61
+ },
62
+ indent=1,
63
+ )
64
+ )
65
+ return 0 if report.ok else 1
66
+
67
+
68
+ def cmd_upload(args: argparse.Namespace) -> int:
69
+ weights = Path(args.dir) / hub.WEIGHTS_FILE if Path(args.dir).is_dir() else Path(args.dir)
70
+ if not weights.is_file():
71
+ print(f"error: no {hub.WEIGHTS_FILE} at {weights}", file=sys.stderr)
72
+ return 2
73
+ readme = Path(args.readme).read_text(encoding="utf-8") if args.readme else None
74
+ sha = hub.upload_weights(
75
+ args.repo,
76
+ str(weights),
77
+ token=os.environ.get("HF_TOKEN"),
78
+ private=args.private,
79
+ readme=readme,
80
+ )
81
+ digest = weights_digest(weights)
82
+ print(
83
+ json.dumps(
84
+ {
85
+ "repo": args.repo,
86
+ "revision": sha,
87
+ "digest": digest,
88
+ "commitment": commitment_.encode(args.repo, sha, digest),
89
+ }
90
+ )
91
+ )
92
+ return 0
93
+
94
+
95
+ def cmd_commit(args: argparse.Namespace) -> int:
96
+ from . import chain as chain_
97
+
98
+ data = commitment_.encode(args.repo, args.revision, args.digest)
99
+ wallet = chain_.wallet(args.wallet_name, args.wallet_hotkey, args.wallet_path)
100
+ chain = chain_.Chain(args.network, args.netuid)
101
+ try:
102
+ chain.commit(wallet, data)
103
+ block = chain.block()
104
+ finally:
105
+ chain.close()
106
+ print(json.dumps({"committed": data, "hotkey": wallet.hotkey.ss58_address, "block": block}))
107
+ return 0
108
+
109
+
110
+ def cmd_submit(args: argparse.Namespace) -> int:
111
+ """check -> upload private -> commit on chain -> publish, in the order that keeps your
112
+ weights yours: the earliest commitment of a set of weights keeps them, so nobody may see the
113
+ file until the chain says it is yours."""
114
+ from . import chain as chain_
115
+
116
+ if cmd_check(args) != 0:
117
+ print("error: the weights did not pass the check the validator runs", file=sys.stderr)
118
+ return 1
119
+ weights = Path(args.dir) / hub.WEIGHTS_FILE if Path(args.dir).is_dir() else Path(args.dir)
120
+ readme = Path(args.readme).read_text(encoding="utf-8") if args.readme else None
121
+ token = os.environ.get("HF_TOKEN")
122
+ sha = hub.upload_weights(args.repo, str(weights), token=token, private=True, readme=readme)
123
+ data = commitment_.encode(args.repo, sha, weights_digest(weights))
124
+ wallet = chain_.wallet(args.wallet_name, args.wallet_hotkey, args.wallet_path)
125
+ chain = chain_.Chain(args.network, args.netuid)
126
+ try:
127
+ chain.commit(wallet, data)
128
+ block = chain.block()
129
+ finally:
130
+ chain.close()
131
+ published = False
132
+ if not args.keep_private:
133
+ from huggingface_hub import HfApi
134
+
135
+ HfApi(token=token).update_repo_settings(args.repo, private=False)
136
+ published = True
137
+ print(
138
+ json.dumps(
139
+ {
140
+ "repo": args.repo,
141
+ "revision": sha,
142
+ "committed": data,
143
+ "block": block,
144
+ "public": published,
145
+ "fee": "none: this subnet charges nothing to submit",
146
+ },
147
+ indent=1,
148
+ )
149
+ )
150
+ return 0
151
+
152
+
153
+ def cmd_status(args: argparse.Namespace) -> int:
154
+ from . import chain as chain_
155
+
156
+ chain = chain_.Chain(args.network, args.netuid)
157
+ try:
158
+ mine = [c for c in chain.commitments() if c.hotkey == args.hotkey]
159
+ finally:
160
+ chain.close()
161
+ out = []
162
+ for c in mine:
163
+ entry = {"hotkey": c.hotkey, "block": c.block, "data": c.data}
164
+ try:
165
+ sub = commitment_.parse(c.data)
166
+ found = hub.inspect(
167
+ sub.repo,
168
+ sub.revision,
169
+ frozenset({hub.WEIGHTS_FILE, "README.md", ".gitattributes"}),
170
+ token=os.environ.get("HF_TOKEN"),
171
+ )
172
+ entry["weights_sha256"] = found.weights_sha256
173
+ entry["digest"] = sub.digest
174
+ entry["digest_matches"] = sub.digest == found.weights_sha256
175
+ except Exception as exc: # noqa: BLE001 - reported, not raised
176
+ entry["problem"] = str(exc)
177
+ out.append(entry)
178
+ print(json.dumps(out, indent=1))
179
+ return 0
180
+
181
+
182
+ def _chain_args(parser: argparse.ArgumentParser) -> None:
183
+ parser.add_argument(
184
+ "--network", default=DEFAULT_NETWORK, help="finney, test, or ws://host:port"
185
+ )
186
+ parser.add_argument("--netuid", type=int, required=True)
187
+
188
+
189
+ def add_subcommands(sub: Any) -> None:
190
+ """Hang a miner's verbs off `sub`, so `attune miner ...` and the standalone command are
191
+ the same parser rather than two that drift."""
192
+ check = sub.add_parser("check", help="check model.safetensors as the validator does")
193
+ check.add_argument(
194
+ "--dir", required=True, help="the directory holding model.safetensors, or the file"
195
+ )
196
+ check.set_defaults(func=cmd_check)
197
+
198
+ upload = sub.add_parser("upload", help="upload model.safetensors; prints the sha to commit")
199
+ upload.add_argument(
200
+ "--dir", required=True, help="the directory holding model.safetensors, or the file"
201
+ )
202
+ upload.add_argument(
203
+ "--repo", required=True, help="your Hugging Face model repository, owner/name"
204
+ )
205
+ upload.add_argument("--readme", default=None, help="a README.md to upload beside it")
206
+ upload.add_argument("--private", action="store_true", help="create the repository private")
207
+ upload.set_defaults(func=cmd_upload)
208
+
209
+ commit = sub.add_parser("commit", help="commit repo@sha on chain for your hotkey")
210
+ commit.add_argument("--repo", required=True)
211
+ commit.add_argument("--revision", required=True, help="the 40-hex commit sha upload printed")
212
+ commit.add_argument(
213
+ "--digest",
214
+ required=True,
215
+ help="the sha256 of model.safetensors (check and upload print it)",
216
+ )
217
+ _chain_args(commit)
218
+ commit.add_argument("--wallet.name", dest="wallet_name", required=True)
219
+ commit.add_argument("--wallet.hotkey", dest="wallet_hotkey", required=True)
220
+ commit.add_argument("--wallet.path", dest="wallet_path", default=None)
221
+ commit.set_defaults(func=cmd_commit)
222
+
223
+ submit = sub.add_parser(
224
+ "submit", help="check, upload private, commit on chain, then publish - in that order"
225
+ )
226
+ submit.add_argument(
227
+ "--dir", required=True, help="the directory holding model.safetensors, or the file"
228
+ )
229
+ submit.add_argument("--repo", required=True, help="your Hugging Face model repository")
230
+ submit.add_argument("--readme", default=None, help="a README.md to upload beside it")
231
+ submit.add_argument(
232
+ "--keep-private",
233
+ action="store_true",
234
+ help="leave the repository private; the validator drops it as missing at its turn",
235
+ )
236
+ _chain_args(submit)
237
+ submit.add_argument("--wallet.name", dest="wallet_name", required=True)
238
+ submit.add_argument("--wallet.hotkey", dest="wallet_hotkey", required=True)
239
+ submit.add_argument("--wallet.path", dest="wallet_path", default=None)
240
+ submit.set_defaults(func=cmd_submit)
241
+
242
+ status = sub.add_parser("status", help="your hotkey's commitment on chain")
243
+ status.add_argument("--hotkey", required=True, help="your hotkey's ss58 address")
244
+ _chain_args(status)
245
+ status.set_defaults(func=cmd_status)
246
+
247
+
248
+ def build_parser() -> argparse.ArgumentParser:
249
+ parser = argparse.ArgumentParser(prog="robotensor-miner", description=__doc__.split("\n")[0])
250
+ add_subcommands(parser.add_subparsers(dest="command", required=True))
251
+ return parser
252
+
253
+
254
+ def main(argv: list[str] | None = None) -> int:
255
+ args = build_parser().parse_args(argv)
256
+ return int(args.func(args) or 0)
257
+
258
+
259
+ if __name__ == "__main__":
260
+ sys.exit(main())
@@ -0,0 +1,51 @@
1
+ """`attune`, as a miner installs it (`pip install robotensor-attune`): a submission, start to end.
2
+
3
+ attune --version
4
+ attune init [DIR]
5
+ attune miner check | upload | commit | submit | status
6
+
7
+ The same `init` and `miner` commands as the validator's install, which has the validator's
8
+ besides. `--role miner` is accepted, so a command written for either one runs here.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from . import __version__
18
+
19
+
20
+ def cmd_init(args: argparse.Namespace) -> int:
21
+ from . import init as init_
22
+
23
+ print(init_.miner(Path(args.directory).expanduser().resolve()))
24
+ return 0
25
+
26
+
27
+ def build_parser() -> argparse.ArgumentParser:
28
+ from . import miner as miner_
29
+
30
+ parser = argparse.ArgumentParser(prog="attune", description=__doc__.split("\n")[0])
31
+ parser.add_argument("--version", action="version", version=f"attune {__version__}")
32
+ sub = parser.add_subparsers(dest="part", required=True)
33
+
34
+ start = sub.add_parser("init", help="a working directory for a submission")
35
+ start.add_argument("directory", nargs="?", default=".", help="where (default: .)")
36
+ start.add_argument("--role", default="miner", choices=("miner",), help=argparse.SUPPRESS)
37
+ start.add_argument("--competition", default=None, help=argparse.SUPPRESS)
38
+ start.set_defaults(func=cmd_init)
39
+
40
+ miner = sub.add_parser("miner", help="check, upload and commit a submission")
41
+ miner_.add_subcommands(miner.add_subparsers(dest="command", required=True))
42
+ return parser
43
+
44
+
45
+ def main(argv: list[str] | None = None) -> int:
46
+ args = build_parser().parse_args(argv)
47
+ return int(args.func(args) or 0)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ sys.exit(main())
File without changes
@@ -0,0 +1,127 @@
1
+ """What a miner writes on chain: one short string naming its submission.
2
+
3
+ vector:<owner>/<name>@<commit>.<digest>
4
+ horizon:<owner>/<name>@<40-hex commit>
5
+
6
+ A Vector commitment names its weights as well as where they are: `<commit>` is the repository
7
+ revision's 20 bytes and `<digest>` the sha256 of its `model.safetensors`, both in unpadded base64url
8
+ (27 and 43 characters), so that a repository id of up to 49 characters fits. The validator refuses
9
+ a commitment whose digest is not what the Hub says that file at that revision hashes to, so a
10
+ miner's commitment pins the exact weights it duels with from the block it was made at.
11
+
12
+ The first word names the competition the submission enters, so one subnet reads both competitions'
13
+ commitments from the same storage and a miner never has to say which netuid slot they meant. The
14
+ string is stored as a raw commitment, which holds at most `MAX_BYTES` bytes, so a repository id can
15
+ be at most about 80 characters. The revision is always a full commit sha: a branch would let a
16
+ miner change what was committed after its block, and the block is what orders the queue.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import base64
22
+ import binascii
23
+ import re
24
+ from dataclasses import dataclass
25
+
26
+ #: A raw commitment's limit on chain (`Raw` data, as `Subtensor.set_commitment` writes it).
27
+ MAX_BYTES = 128
28
+ #: Competition 1, action-grounded in-context learning: the only one live at launch.
29
+ VECTOR = "vector"
30
+ #: Competition 2, video-prompted world-action learning. Its commitments are read and taken in;
31
+ #: what they enter does not pay yet (`config.SHARES`), and the round that scores them is
32
+ #: staged in `lanes/horizon.py`.
33
+ HORIZON = "horizon"
34
+ LANES = (VECTOR, HORIZON)
35
+
36
+ REPO_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*")
37
+ SHA_RE = re.compile(r"[0-9a-f]{40}")
38
+ DIGEST_RE = re.compile(r"[0-9a-f]{64}")
39
+ #: Unpadded base64url of 20 and of 32 bytes.
40
+ B64_RE = {20: re.compile(r"[A-Za-z0-9_-]{27}"), 32: re.compile(r"[A-Za-z0-9_-]{43}")}
41
+
42
+
43
+ class CommitmentError(ValueError):
44
+ """A commitment that is not one this subnet reads."""
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class Submission:
49
+ """A parsed commitment: which lane, which repository, at which commit."""
50
+
51
+ lane: str
52
+ repo: str
53
+ revision: str
54
+ #: The sha256 of the weights, as 64 lowercase hex; None for a lane whose commitment has none.
55
+ digest: str | None = None
56
+
57
+ @property
58
+ def entry(self) -> str:
59
+ return f"{self.repo}@{self.revision}"
60
+
61
+
62
+ def _b64(hex_text: str) -> str:
63
+ return base64.urlsafe_b64encode(bytes.fromhex(hex_text)).rstrip(b"=").decode("ascii")
64
+
65
+
66
+ def _unb64(text: str, size: int) -> str | None:
67
+ """`text` as the hex of `size` bytes, or None when it is not their unpadded base64url."""
68
+ if not B64_RE[size].fullmatch(text):
69
+ return None
70
+ try:
71
+ raw = base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
72
+ except (ValueError, binascii.Error):
73
+ return None
74
+ return raw.hex() if len(raw) == size and _b64(raw.hex()) == text else None
75
+
76
+
77
+ def encode(repo: str, revision: str, digest: str | None = None, lane: str = VECTOR) -> str:
78
+ """The commitment string for `repo@revision` (and, for Vector, the weights' sha256 `digest`);
79
+ `CommitmentError` if it cannot be one."""
80
+ if lane not in LANES:
81
+ raise CommitmentError(f"unknown lane {lane!r}; this subnet reads {', '.join(LANES)}")
82
+ if not REPO_RE.fullmatch(repo):
83
+ raise CommitmentError(f"{repo!r} is not a Hugging Face repo id (owner/name)")
84
+ if not SHA_RE.fullmatch(revision):
85
+ raise CommitmentError(
86
+ f"{revision!r} is not a full 40-hex commit sha; commit the revision the upload printed"
87
+ )
88
+ if lane == VECTOR:
89
+ if digest is None or not DIGEST_RE.fullmatch(digest):
90
+ raise CommitmentError(
91
+ f"{digest!r} is not the 64-hex sha256 of model.safetensors; `check` prints it"
92
+ )
93
+ data = f"{lane}:{repo}@{_b64(revision)}.{_b64(digest)}"
94
+ elif digest is not None:
95
+ raise CommitmentError(f"a {lane} commitment names no digest")
96
+ else:
97
+ data = f"{lane}:{repo}@{revision}"
98
+ if len(data.encode()) > MAX_BYTES:
99
+ raise CommitmentError(
100
+ f"the commitment is {len(data.encode())} bytes, over the chain's {MAX_BYTES}; use a "
101
+ "shorter repository name"
102
+ )
103
+ return data
104
+
105
+
106
+ def parse(data: str) -> Submission:
107
+ """A commitment string as a `Submission`; `CommitmentError` for anything else."""
108
+ if not isinstance(data, str) or len(data.encode()) > MAX_BYTES:
109
+ raise CommitmentError("not a commitment string of at most 128 bytes")
110
+ lane, sep, rest = data.strip().partition(":")
111
+ if not sep or lane not in LANES:
112
+ raise CommitmentError(f"no known lane prefix in {data[:40]!r}")
113
+ repo, at, pinned = rest.rpartition("@")
114
+ if not at or not REPO_RE.fullmatch(repo):
115
+ raise CommitmentError(f"{data[:80]!r} names no <owner>/<name>@...")
116
+ if lane != VECTOR:
117
+ if not SHA_RE.fullmatch(pinned):
118
+ raise CommitmentError(f"{data[:80]!r} is not {lane}:<owner>/<name>@<40-hex sha>")
119
+ return Submission(lane=lane, repo=repo, revision=pinned)
120
+ commit, dot, weights = pinned.partition(".")
121
+ revision, digest = _unb64(commit, 20), _unb64(weights, 32)
122
+ if not dot or revision is None or digest is None:
123
+ raise CommitmentError(
124
+ f"{data[:80]!r} is not vector:<owner>/<name>@<commit>.<digest>: a Vector commitment "
125
+ "names the weights' sha256 too (`attune miner commit --digest`)"
126
+ )
127
+ return Submission(lane=lane, repo=repo, revision=revision, digest=digest)
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.5
2
+ Name: robotensor-attune
3
+ Version: 0.2.1
4
+ Summary: The miner's command for Attune, Robotensor's Bittensor subnet: check, upload and commit a submission.
5
+ Project-URL: Homepage, https://attune.robotensor.ai
6
+ Project-URL: Documentation, https://attune.robotensor.ai/docs/vector
7
+ License: Apache-2.0
8
+ License-File: LICENSE
9
+ Keywords: attune,bittensor,robot-learning,robotics
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: bittensor==10.5.0
16
+ Requires-Dist: huggingface-hub>=1.0
@@ -0,0 +1,17 @@
1
+ robotensor/__init__.py,sha256=4sWsYF8tlgBv7XT2Iad5wwO1EAkCWcNUaXAAiEE2oyc,124
2
+ robotensor/chain.py,sha256=7ndiQUWp92gfbKqbvuIQzylnOtWf-nzrLfUWO_Foqvk,6357
3
+ robotensor/hub.py,sha256=-yez2cbXi6MMKnOUAUEoVw1FkgZDLUW21LGkAfzSgWo,10590
4
+ robotensor/init.py,sha256=d5NVn4JHrDJJ0UXs2NOUMJ8Pu0X1F_9muv7ZJ9C1pGE,2937
5
+ robotensor/miner.py,sha256=liYI_TCKzmlqhVMr_h5Lb3e8z3Zd0g6duaFnKC4t0P0,10017
6
+ robotensor/miner_cli.py,sha256=qMnjdwc_-EXRmdjxuZ66SpG0qQ5K_iBe75QNDos6hKA,1720
7
+ robotensor/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ robotensor/protocol/commitment.py,sha256=2adI8yNfLDg7Moq4J0E0KFACJ6AHQHgBXBpMYX9j0kQ,5639
9
+ vector_runtime/__init__.py,sha256=LGrY1t_lu9bN2eNIKImm_y_jCV0dgvHYm4kLb8IXUfw,391
10
+ vector_runtime/check.py,sha256=_9x6IqfuP033ZHmiqDo1LEk98ILZXeUg9rmZtWnBGDc,6163
11
+ vector_runtime/header.py,sha256=9yfk1-KePWxQGDsyQamu1PrHZCm8G218TwcZqj2YGYE,7172
12
+ vector_runtime/vector_v1.1.json,sha256=1lLhxED_qQ4KBjIObXK_DVky6bRoi6Jvp-7-8hYevqQ,72329
13
+ robotensor_attune-0.2.1.dist-info/METADATA,sha256=SxCY0gEljDilnsbv3Cq8jbAd9F6R8IIImInN7Op3q4g,692
14
+ robotensor_attune-0.2.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
15
+ robotensor_attune-0.2.1.dist-info/entry_points.txt,sha256=g3gTPMl0X-sk3-NL1XDnNVKhFcupBQT8pIZrd16w81A,53
16
+ robotensor_attune-0.2.1.dist-info/licenses/LICENSE,sha256=o1OyBT8s55NsyDfGidngR7fgXAIvQEp6IKDoVRMNJr4,11353
17
+ robotensor_attune-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ attune = robotensor.miner_cli:main