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/__init__.py +3 -0
- robotensor/chain.py +158 -0
- robotensor/hub.py +266 -0
- robotensor/init.py +74 -0
- robotensor/miner.py +260 -0
- robotensor/miner_cli.py +51 -0
- robotensor/protocol/__init__.py +0 -0
- robotensor/protocol/commitment.py +127 -0
- robotensor_attune-0.2.1.dist-info/METADATA +16 -0
- robotensor_attune-0.2.1.dist-info/RECORD +17 -0
- robotensor_attune-0.2.1.dist-info/WHEEL +4 -0
- robotensor_attune-0.2.1.dist-info/entry_points.txt +2 -0
- robotensor_attune-0.2.1.dist-info/licenses/LICENSE +201 -0
- vector_runtime/__init__.py +11 -0
- vector_runtime/check.py +163 -0
- vector_runtime/header.py +191 -0
- vector_runtime/vector_v1.1.json +4826 -0
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())
|
robotensor/miner_cli.py
ADDED
|
@@ -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,,
|