Hub Python Library documentation
Песочницы
Песочницы
Песочницы — это экспериментальная функция. API, значения по умолчанию и поведение могут измениться без предварительного уведомления. Общие песочницы предназначены для рабочих нагрузок в пределах одной границы доверия; их изоляция не гарантирует защиту от всех атак между песочницами. Используйте выделенные песочницы для рабочих нагрузок, которые не доверяют друг другу, и избегайте предоставления рабочим нагрузкам в песочницах долгосрочных или широко охваченных учётных данных.
Песочница — это изолированная облачная машина, которую можно запустить за секунды, выполнять в ней команды с выводом в реальном времени и передавать файлы внутрь и наружу — всё это из Python или через CLI. Песочницы построены на базе Jobs: под капотом песочница — это просто Job, запускающий небольшой сервер, который предоставляет выполнение команд и передачу файлов по HTTP.
Они хорошо подходят в тех случаях, когда нужно выполнять код где-либо, кроме вашей собственной машины:
- Запуск недоверенного или сгенерированного ИИ кода — разрешите агенту выполнять произвольный код, не предоставляя ему доступ к вашей файловой системе. Используйте для этого выделенную песочницу: это отдельная виртуальная машина.
- Воспроизводимые сборки и эксперименты — запуск в чистом, четко определенном образе, на CPU или GPU.
- Распределение нагрузки — запуск сотен параллельных сред (RL-роллауты, оценка, пакетное выполнение инструментов) с минимальными затратами.
Подойдёт любой Docker-образ с /bin/sh — Python, pip или агент не требуют предварительной установки (небольшой статический бинарный файл сервера внедряется при запуске).
Хотите узнать, как это работает под капотом — сервер внутри задания, stateless-аутентификация или то, как одно задание (Job) размещает множество изолированных песочниц? См. концептуальное руководство по песочницам.
Два вида песочниц
Существует два способа получить песочницу. Оба возвращают один и тот же объект Sandbox (с теми же методами run, files, connect, kill); различие состоит лишь в том, как выделяется базовая машина:
| Sandbox.create() — выделенный | SandboxPool — общий / пул | |
|---|---|---|
| mapping | одна задача = одна песочница (целая ВМ) | одна Job = множество песочниц (одна ВМ, упакованы) |
| изоляция | полная ВМ | uid + Landlock (доверие в рамках одного пользователя) |
| холодный старт | ~6 с на песочницу | ~6 с для первого хоста, затем ~1 обмен данными для каждого |
| стоимость | один ВМ на песочницу | один ВМ на хост, распределённый между множеством песочниц |
| GPU | ✅ | ❌ (только CPU) |
| лучше всего подходит для | один песочный контейнер, GPU-нагрузки, недоверенный код | многие дешёвые CPU-песочницы (RL-роллауты, fan-out) |
Ориентир: нужен GPU или запуск кода от недоверенных источников → выделенный. Нужны сотни дешёвых CPU-песочниц → пул.
Быстрый старт
>>> from huggingface_hub import Sandbox
>>> with Sandbox.create() as sbx: # ready in ~6s
... result = sbx.run("python -c 'print(40 + 2)'") # ~100ms per command
... print(result.stdout)
42Выберите любое изображение и вариант оборудования:
>>> sbx = Sandbox.create(image="alpine:3.20")
>>> sbx = Sandbox.create(image="pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel", flavor="a10g-small")Выполнение команд
Sandbox.run() выполняет команду и ожидает её завершения. Передайте строку shell или список argv:
>>> sbx.run("pip install -q numpy") # string → runs through /bin/sh -c
>>> sbx.run(["python", "-c", "import numpy; print(numpy.__version__)"]) # list → exec'd directly
# Live output streaming, plus env, cwd, timeout, stdin
>>> sbx.run("make -j4", cwd="/app", env={"CC": "gcc"}, timeout=600, on_stdout=print, on_stderr=print)По умолчанию режим определяется по типу (строка выполняется через /bin/sh -c, список запускается напрямую как argv). Передайте shell=, чтобы указать его явно — это помогает избежать классической ошибки, когда список из одного элемента, например ["echo hi"], запускается как единая программа с именем "echo hi":
>>> sbx.run("echo $HOME && ls | wc -l", shell=True) # force the shell (pipes, globs, $VARS)
>>> sbx.run(["git", "commit", "-m", msg], shell=False) # force argv (no quoting surprises)shell=True требует строку, а shell=False — список; передача типа данных, не соответствующего требованиям, вызывает ValueError.
Команда, завершающаяся с ненулевым кодом, вызывает SandboxCommandError (с приложенными stdout, stderr и exit_code). Передайте check=False, чтобы получить SandboxCommandResult вместо исключения:
>>> result = sbx.run("test -f /tmp/missing", check=False)
>>> result.exit_code
1Фоновые процессы
Передайте background=True, чтобы запустить длительный процесс (сервер, наблюдатель, процесс обучения), не ожидая его завершения. run сразу возвращает SandboxProcess, а не SandboxCommandResult:
>>> proc = sbx.run("python -m http.server 8000", background=True)
>>> proc
SandboxProcess(pid=1234, cmd='python -m http.server 8000', tag=None, started_at_ms=1700000000000, running=True, exit_code=None)Просмотрите процессы песочницы и остановьте нужный, когда закончите. Завершённые процессы остаются в списке (с running=False и их exit_code) до удаления песочницы, поэтому можно определить, активен ли процесс или он уже завершился:
>>> sbx.processes()
[SandboxProcess(pid=1234, cmd='python -m http.server 8000', tag=None, started_at_ms=1700000000000, running=True, exit_code=None)]
>>> proc.kill()Параметры потоковой передачи и ожидания (timeout, stdin, on_stdout, on_stderr, check) не применяются в фоновом режиме — учитываются только env, cwd и shell.
Файлы
>>> sbx.files.write("/app/script.py", "print('hi')") # str | bytes | file-like
>>> sbx.files.read_text("/app/script.py")
"print('hi')"
>>> sbx.files.upload("local_data.csv", "/data/data.csv") # local -> sandbox
>>> sbx.files.download("/data/results.bin", "results.bin") # sandbox -> local
>>> sbx.files.list("/data")
[FileEntry(name='data.csv', path='/data/data.csv', type='file', size=5324, ...)]Другие вспомогательные функции: stat, exists, mkdir, delete.
Доступ к серверу внутри песочницы
Запустите сервер в песочнице (в фоновом режиме), затем получите к нему доступ извне с помощью Sandbox.proxy_url_for() — запросы перенаправляются сервером песочницы внутри задания на ваш внутренний сервер, поэтому не нужно открывать дополнительный публичный порт. Это работает для обычного HTTP, Server-Sent Events и WebSocket. Используйте URL вместе с Sandbox.proxy_headers для аутентификации (ваш клиент WebSocket/HTTP должен отправлять их):
>>> import httpx2
>>> with Sandbox.create() as sbx:
... sbx.files.write("app.py", "...") # a server exposing e.g. /hello and /ws
... sbx.run("uvicorn app:app --host 127.0.0.1 --port 8000", background=True)
... # plain HTTP
... r = httpx2.get(sbx.proxy_url_for(8000, "/hello"), headers=sbx.proxy_headers)
... # WebSocket: ask for a wss:// URL
... ws_url = sbx.proxy_url_for(8000, "/ws", scheme="wss://")При использовании proxy_headers отключите перенаправления в вашем HTTP-клиенте (follow_redirects=False для httpx или allow_redirects=False для requests). Приложение может перенаправлять запросы на другой источник, и эти пользовательские заголовки аутентификации не должны передаваться туда.
Пул песочниц требует сервер, возвращающий токены возможностей для каждой песочницы. Отсутствие токена является ошибкой; вместо использования токена управления хоста необходимо обновить и перезапустить хост.
То, как внутренний сервер должен слушать, зависит от типа песочницы:
- Выделенный (Sandbox.create()): привязать обычный TCP-порт к
127.0.0.1:<port>. - Пул / общий (SandboxPool): песочницы из пула не могут привязать TCP-порт (Landlock), поэтому слушайте на unix-сокет по адресу
$SBX_PROXY_DIR/<port>.sock(эта переменная окружения задана в каждой песочнице), напримерuvicorn app:app --uds $SBX_PROXY_DIR/8000.sock. Клиентская часть (proxy_url_for/proxy_headers) идентична в обоих случаях.
Жизненный цикл
Песочница переживает процесс, который её создал: вы можете создать её сейчас и переподключиться позже с любого компьютера, на котором хранится тот же HF-токен, без необходимости копировать состояние:
>>> sbx = Sandbox.create()
>>> sbx.id
'687f911eaea852de79c4a50a'
# Later, from anywhere:
>>> sbx = Sandbox.connect("687f911eaea852de79c4a50a")
>>> sbx.kill() # terminate nowidle_timeout(по умолчанию 10 минут) — ключевой параметр: он останавливает песочницу, если не выполняется ни один вызов API и не запущен ни один процесс, поэтому неиспользуемые песочницы перестают тарифицироваться. Установите его при создании (Sandbox.create(idle_timeout="30m")) или передайтеNone, чтобы отключить.- У задания также есть фиксированный максимальный срок жизни 24 часа в качестве жёсткой меры предосторожности (не настраивается).
forward_hf_token=Trueявно передаёт ваш HF-токен в код, выполняющийся в песочнице (в видеHF_TOKEN). Даже приforward_hf_token=Falseне рассматривайте песочницу как жёсткий барьер для ваших учётных данных — см. предупреждение в начале этой страницы.
Многие песочницы одновременно: SandboxPool
Когда требуется много песочниц (параллельные RL-роллауты, fan-out оценка, пакетное выполнение инструментов), назначение отдельного Job на каждую песочницу неэффективно: каждая из них несёт полную стоимость холодного запуска ВМ и занимает целый узел для нагрузки, требующей лишь несколько МБ ОЗУ. SandboxPool размещает множество лёгких песочниц в нескольких общих хост-Job — одна тарифицируемая ВМ обслуживает десятки песочниц, поэтому стоимость на песочницу снижается в соответствующем倍数, а холодный запуск каждой песочницы занимает около одного сетевого round-trip.
>>> from huggingface_hub import SandboxPool
>>> with SandboxPool(image="python:3.12", flavor="cpu-basic", warm_up=2) as pool:
... boxes = [pool.create() for _ in range(100)] # packed across the 2 warm host VMs
... print(boxes[0].run("echo hi").stdout) # each box is a normal Sandbox
hiКаждый вызов create() возвращает один полный Sandbox; для масштабирования вызывайте его многократно. Пул запускает хостовые Jobs по мере необходимости, размещает sandboxes_per_host песочниц на каждом хосте и завершает все процессы при close() (или когда хост становится неактивным, как страховка от лишних затрат). Типичный шаблон масштабирования:
>>> from concurrent.futures import ThreadPoolExecutor
>>> with SandboxPool(image="python:3.12", warm_up=4) as pool:
... boxes = [pool.create() for _ in tasks]
... with ThreadPoolExecutor(32) as ex:
... outputs = list(ex.map(lambda b, t: b.run(t.cmd).stdout, boxes, tasks))Env и idle_timeout задаются для каждого песочного окружения (они относятся к create(), а не к пулу), поэтому песочные окружения в одном пуле могут иметь разные окружения:
>>> sbx = pool.create(env={"SEED": "42"}, idle_timeout="5m", forward_hf_token=True)Объединённый песочный контейнер — это полноценный Sandbox, но некоторые входные параметры задаются хостом, а не для каждой песочницы отдельно, поэтому SandboxPool.create() принимает меньший набор аргументов, чем Sandbox.create():
| Вход | Sandbox.create (выделенный) | SandboxPool.create (пул) |
|---|---|---|
image, flavor | на песочницу | задан пулом (устанавливается один раз на хосте) |
volumes | на песочницу | недоступно (подключено при загрузке хоста) |
env | на песочницу | на песочницу |
idle_timeout | на песочницу | на песочницу |
forward_hf_token | на песочницу | на песочницу |
Пул-песочницы разделяют долгоживущую хост-задачу, поэтому у них нет канала зашифрованных секретов задачи, как у выделенных песочниц. Однако env пул-песочницы передаётся на хост-сервер при создании песочницы (а не при запуске задачи), поэтому он никогда не сохраняется в метаданных какой-либо задачи — передавайте предполагаемые секреты как обычные env.
Рост по запросу и предварительный прогрев
pool.create() создаёт один песочный контейнер за раз, повторно используя хост, на котором ещё есть свободные ресурсы, прежде чем запускать новый, — так вы можете создавать песочные контейнеры по мере поступления задач, и они сами распределяются по уже разогретым хостам:
>>> pool = SandboxPool(image="python:3.12", flavor="cpu-basic")
>>> sbx = pool.create() # boots the first host (~6s)
>>> sbx = pool.create() # packs onto the same warm host (~one round-trip, no new VM)Чтобы избежать холодного запуска хоста при первых нескольких вызовах, заранее подготовьте хосты с помощью warm_up=N (запускаются при первом create()) или предварительным вызовом pool.warm(N).
Повторное использование пулов между процессами и машинами
Тёплые хосты обнаруживаются по меткам задач, поэтому повторное использование работает между процессами: совершенно новый SandboxPool с теми же значениями image/flavor/name подключается к хостам, оставшимся запущенными после предыдущего запуска, вместо того чтобы запускать собственные. Передайте name=, чтобы отдельные пулы не делили хосты.
Чтобы переподключиться с другого компьютера, не имеющего локального состояния, используйте SandboxPool.connect() с указанием pool id — он находит работающий хост, восстанавливает конфигурацию пула (образ, flavor, плотность упаковки) из задания этого хоста и готов create() новые:
>>> pool = SandboxPool.connect("pool-ae9f7efe0bc7") # from anywhere, no config needed
>>> sbx = pool.create()Пул, подключённый через connect(), не владеет общими хостами (их могут использовать другие клиенты), поэтому — как и в случае с Sandbox.connect() — выход из блока with (или вызов close()) освобождает только локальные HTTP-клиенты, оставляя хосты работающими. Хосты пула следует завершать явно с помощью pool delete / hf sandbox pool delete <id>.
Песочницы внутри хоста изолируются с помощью различных uid и наборов правил Landlock для каждой песочницы. Общие песочницы предназначены для параллельных рабочих нагрузок одного и того же пользователя, и изоляция от всех атак между песочницами не гарантируется. Для взаимно недоверенного кода или для GPU используйте Sandbox.create() (отдельная ВМ для каждой песочницы). Компромиссы подробно описаны в концептуальном руководстве: как работает изоляция и — что важнее, если вы выбираете между двумя режимами, — полный список известных ограничений.
Из CLI
Команда hf sandbox повторяет Python API. Выделенный песочный контейнер:
>>> hf sandbox create
✓ Sandbox ready id=687f911eaea852de79c4a50a image=python:3.12 elapsed=6.0s
# Attach labels to the underlying Job
>>> hf sandbox create --label controller-run=run-42 --label team=data-infra
>>> hf sandbox exec 687f911eaea852de79c4a50a -- python -c "print('hi')"
hi
>>> hf sandbox cp data.csv 687f911eaea852de79c4a50a:/data/data.csv
>>> hf sandbox kill 687f911eaea852de79c4a50ahf sandbox exec транслирует вывод в реальном времени и завершается с кодом выхода команды, поэтому его можно использовать в скриптах:
hf sandbox exec $ID -- pytest && echo "tests passed"Запустите длительный процесс в фоновом режиме с помощью hf sandbox spawn (выводит его pid), затем получите список процессов или остановите их. В списке отображается статус каждого процесса (running или exited (<code>)):
>>> hf sandbox spawn $ID -- python -m http.server 8000
✓ Process started sandbox=687f... pid=1234
>>> hf sandbox process ls $ID
pid status cmd
1234 running python -m http.server 8000
>>> hf sandbox process kill $ID 1234Для многих дешёвых общих песочниц прогрейте пул один раз, а затем создавайте в нём по мере необходимости:
# Warm a pool -> prints a pool id (billing starts: a host VM is now running)
>>> hf sandbox pool create python:3.12 --flavor cpu-basic
✓ Pool created id=pool-ae9f7efe0bc7 image=python:3.12 flavor=cpu-basic host=687f... elapsed=5.7s
# Each create packs onto a host with room (found by the pool id, from any machine);
# only when every host is full does it boot a duplicate. Env is per-sandbox (pooled
# sandboxes share a host, so there's no encrypted-secrets channel — use --env).
>>> hf sandbox create --pool pool-ae9f7efe0bc7 --env LOG_LEVEL=debug
>>> hf sandbox create --pool pool-ae9f7efe0bc7
>>> hf sandbox pool ls
>>> hf sandbox pool delete pool-ae9f7efe0bc7 # terminate the pool's hosts (and their sandboxes)hf sandbox create --pool создаёт общий sandbox; его id имеет вид <host_job_id>.<local_id> и работает везде, где используется выделенный id (exec, cp, kill). Пул не имеет локального состояния — это просто его работающие хостовые ВМ, которые находятся по id пула, — поэтому он работает с любой машины и перестаёт существовать, как только все его хосты исчезают (завершены или истёк таймаут простоя).