pytorch

Форк
0
/
nightly.py 
703 строки · 22.0 Кб
1
#!/usr/bin/env python3
2
# Much of the logging code here was forked from https://github.com/ezyang/ghstack
3
# Copyright (c) Edward Z. Yang <ezyang@mit.edu>
4
"""Checks out the nightly development version of PyTorch and installs pre-built
5
binaries into the repo.
6

7
You can use this script to check out a new nightly branch with the following::
8

9
    $ ./tools/nightly.py checkout -b my-nightly-branch
10
    $ conda activate pytorch-deps
11

12
Or if you would like to re-use an existing conda environment, you can pass in
13
the regular environment parameters (--name or --prefix)::
14

15
    $ ./tools/nightly.py checkout -b my-nightly-branch -n my-env
16
    $ conda activate my-env
17

18
You can also use this tool to pull the nightly commits into the current branch as
19
well. This can be done with
20

21
    $ ./tools/nightly.py pull -n my-env
22
    $ conda activate my-env
23

24
Pulling will reinstalle the conda dependencies as well as the nightly binaries into
25
the repo directory.
26
"""
27
import contextlib
28
import datetime
29
import functools
30
import glob
31
import json
32
import logging
33
import os
34
import re
35
import shutil
36
import subprocess
37
import sys
38
import tempfile
39
import time
40
import uuid
41
from argparse import ArgumentParser
42
from ast import literal_eval
43
from typing import (
44
    Any,
45
    Callable,
46
    cast,
47
    Dict,
48
    Generator,
49
    Iterable,
50
    Iterator,
51
    List,
52
    Optional,
53
    Sequence,
54
    Set,
55
    Tuple,
56
    TypeVar,
57
)
58

59
LOGGER: Optional[logging.Logger] = None
60
URL_FORMAT = "{base_url}/{platform}/{dist_name}.tar.bz2"
61
DATETIME_FORMAT = "%Y-%m-%d_%Hh%Mm%Ss"
62
SHA1_RE = re.compile("([0-9a-fA-F]{40})")
63
USERNAME_PASSWORD_RE = re.compile(r":\/\/(.*?)\@")
64
LOG_DIRNAME_RE = re.compile(
65
    r"(\d{4}-\d\d-\d\d_\d\dh\d\dm\d\ds)_" r"[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}"
66
)
67
SPECS_TO_INSTALL = ("pytorch", "mypy", "pytest", "hypothesis", "ipython", "sphinx")
68

69

70
class Formatter(logging.Formatter):
71
    redactions: Dict[str, str]
72

73
    def __init__(self, fmt: Optional[str] = None, datefmt: Optional[str] = None):
74
        super().__init__(fmt, datefmt)
75
        self.redactions = {}
76

77
    # Remove sensitive information from URLs
78
    def _filter(self, s: str) -> str:
79
        s = USERNAME_PASSWORD_RE.sub(r"://<USERNAME>:<PASSWORD>@", s)
80
        for needle, replace in self.redactions.items():
81
            s = s.replace(needle, replace)
82
        return s
83

84
    def formatMessage(self, record: logging.LogRecord) -> str:
85
        if record.levelno == logging.INFO or record.levelno == logging.DEBUG:
86
            # Log INFO/DEBUG without any adornment
87
            return record.getMessage()
88
        else:
89
            # I'm not sure why, but formatMessage doesn't show up
90
            # even though it's in the typeshed for Python >3
91
            return super().formatMessage(record)
92

93
    def format(self, record: logging.LogRecord) -> str:
94
        return self._filter(super().format(record))
95

96
    def redact(self, needle: str, replace: str = "<REDACTED>") -> None:
97
        """Redact specific strings; e.g., authorization tokens.  This won't
98
        retroactively redact stuff you've already leaked, so make sure
99
        you redact things as soon as possible.
100
        """
101
        # Don't redact empty strings; this will lead to something
102
        # that looks like s<REDACTED>t<REDACTED>r<REDACTED>...
103
        if needle == "":
104
            return
105
        self.redactions[needle] = replace
106

107

108
@functools.lru_cache
109
def logging_base_dir() -> str:
110
    meta_dir = os.getcwd()
111
    base_dir = os.path.join(meta_dir, "nightly", "log")
112
    os.makedirs(base_dir, exist_ok=True)
113
    return base_dir
114

115

116
@functools.lru_cache
117
def logging_run_dir() -> str:
118
    cur_dir = os.path.join(
119
        logging_base_dir(),
120
        f"{datetime.datetime.now().strftime(DATETIME_FORMAT)}_{uuid.uuid1()}",
121
    )
122
    os.makedirs(cur_dir, exist_ok=True)
123
    return cur_dir
124

125

126
@functools.lru_cache
127
def logging_record_argv() -> None:
128
    s = subprocess.list2cmdline(sys.argv)
129
    with open(os.path.join(logging_run_dir(), "argv"), "w") as f:
130
        f.write(s)
131

132

133
def logging_record_exception(e: BaseException) -> None:
134
    with open(os.path.join(logging_run_dir(), "exception"), "w") as f:
135
        f.write(type(e).__name__)
136

137

138
def logging_rotate() -> None:
139
    log_base = logging_base_dir()
140
    old_logs = os.listdir(log_base)
141
    old_logs.sort(reverse=True)
142
    for stale_log in old_logs[1000:]:
143
        # Sanity check that it looks like a log
144
        if LOG_DIRNAME_RE.fullmatch(stale_log) is not None:
145
            shutil.rmtree(os.path.join(log_base, stale_log))
146

147

148
@contextlib.contextmanager
149
def logging_manager(*, debug: bool = False) -> Generator[logging.Logger, None, None]:
150
    """Setup logging. If a failure starts here we won't
151
    be able to save the user in a reasonable way.
152

153
    Logging structure: there is one logger (the root logger)
154
    and in processes all events.  There are two handlers:
155
    stderr (INFO) and file handler (DEBUG).
156
    """
157
    formatter = Formatter(fmt="%(levelname)s: %(message)s", datefmt="")
158
    root_logger = logging.getLogger("conda-pytorch")
159
    root_logger.setLevel(logging.DEBUG)
160

161
    console_handler = logging.StreamHandler()
162
    if debug:
163
        console_handler.setLevel(logging.DEBUG)
164
    else:
165
        console_handler.setLevel(logging.INFO)
166
    console_handler.setFormatter(formatter)
167
    root_logger.addHandler(console_handler)
168

169
    log_file = os.path.join(logging_run_dir(), "nightly.log")
170

171
    file_handler = logging.FileHandler(log_file)
172
    file_handler.setFormatter(formatter)
173
    root_logger.addHandler(file_handler)
174
    logging_record_argv()
175

176
    try:
177
        logging_rotate()
178
        print(f"log file: {log_file}")
179
        yield root_logger
180
    except Exception as e:
181
        logging.exception("Fatal exception")
182
        logging_record_exception(e)
183
        print(f"log file: {log_file}")
184
        sys.exit(1)
185
    except BaseException as e:
186
        # You could logging.debug here to suppress the backtrace
187
        # entirely, but there is no reason to hide it from technically
188
        # savvy users.
189
        logging.info("", exc_info=True)
190
        logging_record_exception(e)
191
        print(f"log file: {log_file}")
192
        sys.exit(1)
193

194

195
def check_in_repo() -> Optional[str]:
196
    """Ensures that we are in the PyTorch repo."""
197
    if not os.path.isfile("setup.py"):
198
        return "Not in root-level PyTorch repo, no setup.py found"
199
    with open("setup.py") as f:
200
        s = f.read()
201
    if "PyTorch" not in s:
202
        return "Not in PyTorch repo, 'PyTorch' not found in setup.py"
203
    return None
204

205

206
def check_branch(subcommand: str, branch: Optional[str]) -> Optional[str]:
207
    """Checks that the branch name can be checked out."""
208
    if subcommand != "checkout":
209
        return None
210
    # first make sure actual branch name was given
211
    if branch is None:
212
        return "Branch name to checkout must be supplied with '-b' option"
213
    # next check that the local repo is clean
214
    cmd = ["git", "status", "--untracked-files=no", "--porcelain"]
215
    p = subprocess.run(
216
        cmd,
217
        capture_output=True,
218
        check=True,
219
        text=True,
220
    )
221
    if p.stdout.strip():
222
        return "Need to have clean working tree to checkout!\n\n" + p.stdout
223
    # next check that the branch name doesn't already exist
224
    cmd = ["git", "show-ref", "--verify", "--quiet", "refs/heads/" + branch]
225
    p = subprocess.run(cmd, capture_output=True, check=False)  # type: ignore[assignment]
226
    if not p.returncode:
227
        return f"Branch {branch!r} already exists"
228
    return None
229

230

231
@contextlib.contextmanager
232
def timer(logger: logging.Logger, prefix: str) -> Iterator[None]:
233
    """Timed context manager"""
234
    start_time = time.time()
235
    yield
236
    logger.info("%s took %.3f [s]", prefix, time.time() - start_time)
237

238

239
F = TypeVar("F", bound=Callable[..., Any])
240

241

242
def timed(prefix: str) -> Callable[[F], F]:
243
    """Decorator for timing functions"""
244

245
    def dec(f: F) -> F:
246
        @functools.wraps(f)
247
        def wrapper(*args: Any, **kwargs: Any) -> Any:
248
            global LOGGER
249
            logger = cast(logging.Logger, LOGGER)
250
            logger.info(prefix)
251
            with timer(logger, prefix):
252
                return f(*args, **kwargs)
253

254
        return cast(F, wrapper)
255

256
    return dec
257

258

259
def _make_channel_args(
260
    channels: Iterable[str] = ("pytorch-nightly",),
261
    override_channels: bool = False,
262
) -> List[str]:
263
    args = []
264
    for channel in channels:
265
        args.append("--channel")
266
        args.append(channel)
267
    if override_channels:
268
        args.append("--override-channels")
269
    return args
270

271

272
@timed("Solving conda environment")
273
def conda_solve(
274
    name: Optional[str] = None,
275
    prefix: Optional[str] = None,
276
    channels: Iterable[str] = ("pytorch-nightly",),
277
    override_channels: bool = False,
278
) -> Tuple[List[str], str, str, bool, List[str]]:
279
    """Performs the conda solve and splits the deps from the package."""
280
    # compute what environment to use
281
    if prefix is not None:
282
        existing_env = True
283
        env_opts = ["--prefix", prefix]
284
    elif name is not None:
285
        existing_env = True
286
        env_opts = ["--name", name]
287
    else:
288
        # create new environment
289
        existing_env = False
290
        env_opts = ["--name", "pytorch-deps"]
291
    # run solve
292
    if existing_env:
293
        cmd = [
294
            "conda",
295
            "install",
296
            "--yes",
297
            "--dry-run",
298
            "--json",
299
        ]
300
        cmd.extend(env_opts)
301
    else:
302
        cmd = [
303
            "conda",
304
            "create",
305
            "--yes",
306
            "--dry-run",
307
            "--json",
308
            "--name",
309
            "__pytorch__",
310
        ]
311
    channel_args = _make_channel_args(
312
        channels=channels, override_channels=override_channels
313
    )
314
    cmd.extend(channel_args)
315
    cmd.extend(SPECS_TO_INSTALL)
316
    p = subprocess.run(cmd, capture_output=True, check=True)
317
    # parse solution
318
    solve = json.loads(p.stdout)
319
    link = solve["actions"]["LINK"]
320
    deps = []
321
    for pkg in link:
322
        url = URL_FORMAT.format(**pkg)
323
        if pkg["name"] == "pytorch":
324
            pytorch = url
325
            platform = pkg["platform"]
326
        else:
327
            deps.append(url)
328
    return deps, pytorch, platform, existing_env, env_opts
329

330

331
@timed("Installing dependencies")
332
def deps_install(deps: List[str], existing_env: bool, env_opts: List[str]) -> None:
333
    """Install dependencies to deps environment"""
334
    if not existing_env:
335
        # first remove previous pytorch-deps env
336
        cmd = ["conda", "env", "remove", "--yes"] + env_opts
337
        p = subprocess.run(cmd, check=True)
338
    # install new deps
339
    inst_opt = "install" if existing_env else "create"
340
    cmd = ["conda", inst_opt, "--yes", "--no-deps"] + env_opts + deps
341
    p = subprocess.run(cmd, check=True)
342

343

344
@timed("Installing pytorch nightly binaries")
345
def pytorch_install(url: str) -> "tempfile.TemporaryDirectory[str]":
346
    """Install pytorch into a temporary directory"""
347
    pytdir = tempfile.TemporaryDirectory()
348
    cmd = ["conda", "create", "--yes", "--no-deps", "--prefix", pytdir.name, url]
349
    p = subprocess.run(cmd, check=True)
350
    return pytdir
351

352

353
def _site_packages(dirname: str, platform: str) -> str:
354
    if platform.startswith("win"):
355
        template = os.path.join(dirname, "Lib", "site-packages")
356
    else:
357
        template = os.path.join(dirname, "lib", "python*.*", "site-packages")
358
    spdir = glob.glob(template)[0]
359
    return spdir
360

361

362
def _ensure_commit(git_sha1: str) -> None:
363
    """Make sure that we actually have the commit locally"""
364
    cmd = ["git", "cat-file", "-e", git_sha1 + "^{commit}"]
365
    p = subprocess.run(cmd, capture_output=True, check=False)
366
    if p.returncode == 0:
367
        # we have the commit locally
368
        return
369
    # we don't have the commit, must fetch
370
    cmd = ["git", "fetch", "https://github.com/pytorch/pytorch.git", git_sha1]
371
    p = subprocess.run(cmd, check=True)
372

373

374
def _nightly_version(spdir: str) -> str:
375
    # first get the git version from the installed module
376
    version_fname = os.path.join(spdir, "torch", "version.py")
377
    with open(version_fname) as f:
378
        lines = f.read().splitlines()
379
    for line in lines:
380
        if not line.startswith("git_version"):
381
            continue
382
        git_version = literal_eval(line.partition("=")[2].strip())
383
        break
384
    else:
385
        raise RuntimeError(f"Could not find git_version in {version_fname}")
386
    print(f"Found released git version {git_version}")
387
    # now cross reference with nightly version
388
    _ensure_commit(git_version)
389
    cmd = ["git", "show", "--no-patch", "--format=%s", git_version]
390
    p = subprocess.run(
391
        cmd,
392
        capture_output=True,
393
        check=True,
394
        text=True,
395
    )
396
    m = SHA1_RE.search(p.stdout)
397
    if m is None:
398
        raise RuntimeError(
399
            f"Could not find nightly release in git history:\n  {p.stdout}"
400
        )
401
    nightly_version = m.group(1)
402
    print(f"Found nightly release version {nightly_version}")
403
    # now checkout nightly version
404
    _ensure_commit(nightly_version)
405
    return nightly_version
406

407

408
@timed("Checking out nightly PyTorch")
409
def checkout_nightly_version(branch: str, spdir: str) -> None:
410
    """Get's the nightly version and then checks it out."""
411
    nightly_version = _nightly_version(spdir)
412
    cmd = ["git", "checkout", "-b", branch, nightly_version]
413
    p = subprocess.run(cmd, check=True)
414

415

416
@timed("Pulling nightly PyTorch")
417
def pull_nightly_version(spdir: str) -> None:
418
    """Fetches the nightly version and then merges it ."""
419
    nightly_version = _nightly_version(spdir)
420
    cmd = ["git", "merge", nightly_version]
421
    p = subprocess.run(cmd, check=True)
422

423

424
def _get_listing_linux(source_dir: str) -> List[str]:
425
    listing = glob.glob(os.path.join(source_dir, "*.so"))
426
    listing.extend(glob.glob(os.path.join(source_dir, "lib", "*.so")))
427
    return listing
428

429

430
def _get_listing_osx(source_dir: str) -> List[str]:
431
    # oddly, these are .so files even on Mac
432
    listing = glob.glob(os.path.join(source_dir, "*.so"))
433
    listing.extend(glob.glob(os.path.join(source_dir, "lib", "*.dylib")))
434
    return listing
435

436

437
def _get_listing_win(source_dir: str) -> List[str]:
438
    listing = glob.glob(os.path.join(source_dir, "*.pyd"))
439
    listing.extend(glob.glob(os.path.join(source_dir, "lib", "*.lib")))
440
    listing.extend(glob.glob(os.path.join(source_dir, "lib", "*.dll")))
441
    return listing
442

443

444
def _glob_pyis(d: str) -> Set[str]:
445
    search = os.path.join(d, "**", "*.pyi")
446
    pyis = {os.path.relpath(p, d) for p in glob.iglob(search)}
447
    return pyis
448

449

450
def _find_missing_pyi(source_dir: str, target_dir: str) -> List[str]:
451
    source_pyis = _glob_pyis(source_dir)
452
    target_pyis = _glob_pyis(target_dir)
453
    missing_pyis = [os.path.join(source_dir, p) for p in (source_pyis - target_pyis)]
454
    missing_pyis.sort()
455
    return missing_pyis
456

457

458
def _get_listing(source_dir: str, target_dir: str, platform: str) -> List[str]:
459
    if platform.startswith("linux"):
460
        listing = _get_listing_linux(source_dir)
461
    elif platform.startswith("osx"):
462
        listing = _get_listing_osx(source_dir)
463
    elif platform.startswith("win"):
464
        listing = _get_listing_win(source_dir)
465
    else:
466
        raise RuntimeError(f"Platform {platform!r} not recognized")
467
    listing.extend(_find_missing_pyi(source_dir, target_dir))
468
    listing.append(os.path.join(source_dir, "version.py"))
469
    listing.append(os.path.join(source_dir, "testing", "_internal", "generated"))
470
    listing.append(os.path.join(source_dir, "bin"))
471
    listing.append(os.path.join(source_dir, "include"))
472
    return listing
473

474

475
def _remove_existing(trg: str, is_dir: bool) -> None:
476
    if os.path.exists(trg):
477
        if is_dir:
478
            shutil.rmtree(trg)
479
        else:
480
            os.remove(trg)
481

482

483
def _move_single(
484
    src: str,
485
    source_dir: str,
486
    target_dir: str,
487
    mover: Callable[[str, str], None],
488
    verb: str,
489
) -> None:
490
    is_dir = os.path.isdir(src)
491
    relpath = os.path.relpath(src, source_dir)
492
    trg = os.path.join(target_dir, relpath)
493
    _remove_existing(trg, is_dir)
494
    # move over new files
495
    if is_dir:
496
        os.makedirs(trg, exist_ok=True)
497
        for root, dirs, files in os.walk(src):
498
            relroot = os.path.relpath(root, src)
499
            for name in files:
500
                relname = os.path.join(relroot, name)
501
                s = os.path.join(src, relname)
502
                t = os.path.join(trg, relname)
503
                print(f"{verb} {s} -> {t}")
504
                mover(s, t)
505
            for name in dirs:
506
                relname = os.path.join(relroot, name)
507
                os.makedirs(os.path.join(trg, relname), exist_ok=True)
508
    else:
509
        print(f"{verb} {src} -> {trg}")
510
        mover(src, trg)
511

512

513
def _copy_files(listing: List[str], source_dir: str, target_dir: str) -> None:
514
    for src in listing:
515
        _move_single(src, source_dir, target_dir, shutil.copy2, "Copying")
516

517

518
def _link_files(listing: List[str], source_dir: str, target_dir: str) -> None:
519
    for src in listing:
520
        _move_single(src, source_dir, target_dir, os.link, "Linking")
521

522

523
@timed("Moving nightly files into repo")
524
def move_nightly_files(spdir: str, platform: str) -> None:
525
    """Moves PyTorch files from temporary installed location to repo."""
526
    # get file listing
527
    source_dir = os.path.join(spdir, "torch")
528
    target_dir = os.path.abspath("torch")
529
    listing = _get_listing(source_dir, target_dir, platform)
530
    # copy / link files
531
    if platform.startswith("win"):
532
        _copy_files(listing, source_dir, target_dir)
533
    else:
534
        try:
535
            _link_files(listing, source_dir, target_dir)
536
        except Exception:
537
            _copy_files(listing, source_dir, target_dir)
538

539

540
def _available_envs() -> Dict[str, str]:
541
    cmd = ["conda", "env", "list"]
542
    p = subprocess.run(
543
        cmd,
544
        check=True,
545
        capture_output=True,
546
        text=True,
547
    )
548
    lines = p.stdout.splitlines()
549
    envs = {}
550
    for line in map(str.strip, lines):
551
        if not line or line.startswith("#"):
552
            continue
553
        parts = line.split()
554
        if len(parts) == 1:
555
            # unnamed env
556
            continue
557
        envs[parts[0]] = parts[-1]
558
    return envs
559

560

561
@timed("Writing pytorch-nightly.pth")
562
def write_pth(env_opts: List[str], platform: str) -> None:
563
    """Writes Python path file for this dir."""
564
    env_type, env_dir = env_opts
565
    if env_type == "--name":
566
        # have to find directory
567
        envs = _available_envs()
568
        env_dir = envs[env_dir]
569
    spdir = _site_packages(env_dir, platform)
570
    pth = os.path.join(spdir, "pytorch-nightly.pth")
571
    s = (
572
        "# This file was autogenerated by PyTorch's tools/nightly.py\n"
573
        "# Please delete this file if you no longer need the following development\n"
574
        "# version of PyTorch to be importable\n"
575
        f"{os.getcwd()}\n"
576
    )
577
    with open(pth, "w") as f:
578
        f.write(s)
579

580

581
def install(
582
    *,
583
    logger: logging.Logger,
584
    subcommand: str = "checkout",
585
    branch: Optional[str] = None,
586
    name: Optional[str] = None,
587
    prefix: Optional[str] = None,
588
    channels: Iterable[str] = ("pytorch-nightly",),
589
    override_channels: bool = False,
590
) -> None:
591
    """Development install of PyTorch"""
592
    deps, pytorch, platform, existing_env, env_opts = conda_solve(
593
        name=name, prefix=prefix, channels=channels, override_channels=override_channels
594
    )
595
    if deps:
596
        deps_install(deps, existing_env, env_opts)
597
    pytdir = pytorch_install(pytorch)
598
    spdir = _site_packages(pytdir.name, platform)
599
    if subcommand == "checkout":
600
        checkout_nightly_version(cast(str, branch), spdir)
601
    elif subcommand == "pull":
602
        pull_nightly_version(spdir)
603
    else:
604
        raise ValueError(f"Subcommand {subcommand} must be one of: checkout, pull.")
605
    move_nightly_files(spdir, platform)
606
    write_pth(env_opts, platform)
607
    pytdir.cleanup()
608
    logger.info(
609
        "-------\nPyTorch Development Environment set up!\nPlease activate to "
610
        "enable this environment:\n  $ conda activate %s",
611
        env_opts[1],
612
    )
613

614

615
def make_parser() -> ArgumentParser:
616
    p = ArgumentParser("nightly")
617
    # subcommands
618
    subcmd = p.add_subparsers(dest="subcmd", help="subcommand to execute")
619
    co = subcmd.add_parser("checkout", help="checkout a new branch")
620
    co.add_argument(
621
        "-b",
622
        "--branch",
623
        help="Branch name to checkout",
624
        dest="branch",
625
        default=None,
626
        metavar="NAME",
627
    )
628
    pull = subcmd.add_parser(
629
        "pull", help="pulls the nightly commits into the current branch"
630
    )
631
    # general arguments
632
    subps = [co, pull]
633
    for subp in subps:
634
        subp.add_argument(
635
            "-n",
636
            "--name",
637
            help="Name of environment",
638
            dest="name",
639
            default=None,
640
            metavar="ENVIRONMENT",
641
        )
642
        subp.add_argument(
643
            "-p",
644
            "--prefix",
645
            help="Full path to environment location (i.e. prefix)",
646
            dest="prefix",
647
            default=None,
648
            metavar="PATH",
649
        )
650
        subp.add_argument(
651
            "-v",
652
            "--verbose",
653
            help="Provide debugging info",
654
            dest="verbose",
655
            default=False,
656
            action="store_true",
657
        )
658
        subp.add_argument(
659
            "--override-channels",
660
            help="Do not search default or .condarc channels.",
661
            dest="override_channels",
662
            default=False,
663
            action="store_true",
664
        )
665
        subp.add_argument(
666
            "-c",
667
            "--channel",
668
            help="Additional channel to search for packages. 'pytorch-nightly' will always be prepended to this list.",
669
            dest="channels",
670
            action="append",
671
            metavar="CHANNEL",
672
        )
673
    return p
674

675

676
def main(args: Optional[Sequence[str]] = None) -> None:
677
    """Main entry point"""
678
    global LOGGER
679
    p = make_parser()
680
    ns = p.parse_args(args)
681
    ns.branch = getattr(ns, "branch", None)
682
    status = check_in_repo()
683
    status = status or check_branch(ns.subcmd, ns.branch)
684
    if status:
685
        sys.exit(status)
686
    channels = ["pytorch-nightly"]
687
    if ns.channels:
688
        channels.extend(ns.channels)
689
    with logging_manager(debug=ns.verbose) as logger:
690
        LOGGER = logger
691
        install(
692
            subcommand=ns.subcmd,
693
            branch=ns.branch,
694
            name=ns.name,
695
            prefix=ns.prefix,
696
            logger=logger,
697
            channels=channels,
698
            override_channels=ns.override_channels,
699
        )
700

701

702
if __name__ == "__main__":
703
    main()
704

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.