setup models

This commit is contained in:
Marcos Lohmann
2026-10-07 13:43:09 -03:00
parent aeee6c7de1
commit 84ea9a5307
75 changed files with 5093 additions and 58 deletions

53
infra/ds/build_env.sh Executable file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Job de build (rede gerenciada, com internet): cria o ambiente Python do candidato com as
# versões fixadas de env/requirements/, empacota e publica em dmb-artefatos/envs/<candidato>/.
# Os jobs de benchmark (sub-rede privada) só baixam esse pacote; nunca instalam da internet.
set -euo pipefail
CAND="${DMB_CANDIDATE:?defina DMB_CANDIDATE}"
CODE="$(cd "$(dirname "$0")/../.." && pwd)"
BASE_PY=/opt/conda/bin/python3
ENV_DIR="$HOME/envs/$CAND"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
cd "$CODE"
echo "== build $CAND em $ENV_DIR (código $DMB_CODE_REV)"
rm -rf "$ENV_DIR"
"$BASE_PY" -m venv "$ENV_DIR"
"$ENV_DIR/bin/pip" install -q --upgrade "pip==25.2"
"$ENV_DIR/bin/pip" install -q -r env/requirements/common.txt "pytest==8.4.2" "httpx==0.28.1"
"$ENV_DIR/bin/pip" install -q --extra-index-url https://download.pytorch.org/whl/cu128 -r "env/requirements/$CAND.txt"
src_object() { # objeto do tarball de código-fonte do candidato, segundo o manifesto
"$BASE_PY" -c "import json,sys; m=json.load(open('env/artifacts-manifest.json')); print(next(a['object'] for a in m['artifacts'] if a['candidate']=='$CAND' and a['kind']=='source'))"
}
case "$CAND" in
semif|rizzo_flow)
OBJ="$(src_object)"
"$BASE_PY" -m harness.objstore get dmb-artefatos "$OBJ" /tmp/src.tar.gz
mkdir -p /tmp/src && tar xzf /tmp/src.tar.gz -C /tmp/src
"$ENV_DIR/bin/pip" install -q --no-deps /tmp/src/*/
;;
esac
if [ "$CAND" = rizzo_flow ]; then
# Runtime llama.cpp b11081 (CPU e CUDA), com o SHA-256 conferido pelo próprio Rizzo Flow.
(cd "$ENV_DIR" && "$ENV_DIR/bin/rizzo" download --only runtime --runtime cpu \
&& "$ENV_DIR/bin/rizzo" download --only runtime --runtime cuda && rm -rf runtimes/downloads)
fi
"$ENV_DIR/bin/python" -m harness.image_manifest > "$ENV_DIR/env-manifest.json"
"$ENV_DIR/bin/python" -m pytest -q tests 2>&1 | tail -2
TARBALL="/tmp/$CAND-env.tar.gz"
tar czf "$TARBALL" -C "$HOME" "envs/$CAND"
OBJECT="envs/$CAND/$STAMP.tar.gz"
SHA="$("$BASE_PY" -m harness.objstore put dmb-artefatos "$OBJECT" "$TARBALL" --sha256)"
SIZE="$(stat -c %s "$TARBALL")"
cat > /tmp/latest.json <<JSON
{"candidate": "$CAND", "object": "$OBJECT", "sha256": "$SHA", "bytes": $SIZE,
"built_at": "$STAMP", "code_rev": "${DMB_CODE_REV:-}", "python": "$("$ENV_DIR/bin/python" --version 2>&1)",
"home": "$HOME"}
JSON
"$BASE_PY" -m harness.objstore put dmb-artefatos "envs/$CAND/latest.json" /tmp/latest.json
"$BASE_PY" -m harness.objstore put dmb-artefatos "envs/$CAND/$STAMP-env-manifest.json" "$ENV_DIR/env-manifest.json"
cat /tmp/latest.json
echo "== ok"

51
infra/ds/build_llama_cuda.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Job de build (rede gerenciada): compila o llama.cpp com CUDA para a imagem dos jobs do Data Science.
# Os binários oficiais CUDA do llama.cpp exigem GLIBC 2.38, ausente nessa imagem. Compilamos o mesmo
# commit fixado pelo Rizzo Flow, com sysroot glibc 2.17, libstdc++ estática e CUDA 12.8 (sm_80/sm_86),
# no mesmo layout dos pacotes oficiais (backends como plug-ins, libs CUDA no mesmo diretório).
set -euo pipefail
COMMIT=161755f29e415e2c33efe906e91843c068efd664 # rizzo_flow.llama_release.COMMIT (b11081)
CODE="$(cd "$(dirname "$0")/../.." && pwd)"
BASE_PY=/opt/conda/bin/python3
TC="$HOME/toolchain"
OUT_NAME="llama-b11081-linux-x64-cuda-dsjobs"
OUT="$HOME/$OUT_NAME"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
echo "== glibc da imagem: $(ldd --version | head -1)"
/opt/conda/bin/conda create -q -y -p "$TC" -c conda-forge \
"cuda-nvcc=12.8" "cuda-cudart-dev=12.8" "libcublas-dev=12.8" "cuda-version=12.8" \
cmake ninja git "gxx_linux-64=13" "sysroot_linux-64=2.17" >/dev/null
export PATH="$TC/bin:$PATH" CUDAToolkit_ROOT="$TC" CUDACXX="$TC/bin/nvcc"
export CC="$TC/bin/x86_64-conda-linux-gnu-gcc" CXX="$TC/bin/x86_64-conda-linux-gnu-g++"
git clone -q https://github.com/ggml-org/llama.cpp /tmp/llama.cpp
git -C /tmp/llama.cpp checkout -q "$COMMIT"
cmake -S /tmp/llama.cpp -B /tmp/build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON -DGGML_BACKEND_DL=ON -DGGML_NATIVE=OFF -DGGML_CPU_ALL_VARIANTS=ON \
-DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES="80;86" -DLLAMA_CURL=OFF \
-DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_SERVER=OFF -DLLAMA_BUILD_TOOLS=OFF \
-DCMAKE_SHARED_LINKER_FLAGS="-static-libstdc++ -static-libgcc" \
-DCMAKE_BUILD_RPATH_USE_ORIGIN=ON -DCMAKE_INSTALL_RPATH='$ORIGIN' -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
>/tmp/cmake.log
cmake --build /tmp/build -j "$(nproc)" >/tmp/build.log 2>&1 || { tail -50 /tmp/build.log; exit 1; }
rm -rf "$OUT" && mkdir -p "$OUT"
find /tmp/build/bin -maxdepth 1 -name "*.so" -exec cp -L {} "$OUT/" \;
for lib in libcudart.so.12 libcublas.so.12 libcublasLt.so.12; do cp -L "$TC/lib/$lib" "$OUT/"; done
ls -la "$OUT"
echo "== dependências não resolvidas:"; for f in "$OUT"/*.so*; do LD_LIBRARY_PATH="$OUT" ldd "$f" | grep "not found" || true; done
echo "== maior GLIBC exigida:"; objdump -T "$OUT"/*.so* 2>/dev/null | grep -o "GLIBC_[0-9.]*" | sort -uV | tail -1
cd "$CODE"
TARBALL="/tmp/$OUT_NAME.tar.gz"
tar czf "$TARBALL" -C "$HOME" "$OUT_NAME"
OBJECT="runtimes/rizzo_flow/$OUT_NAME-$STAMP.tar.gz"
SHA="$("$BASE_PY" -m harness.objstore put dmb-artefatos "$OBJECT" "$TARBALL" --sha256)"
cat > /tmp/cuda-latest.json <<JSON
{"object": "$OBJECT", "sha256": "$SHA", "dir": "$OUT_NAME", "llama_commit": "$COMMIT",
"cuda": "12.8", "archs": "80;86", "built_at": "$STAMP", "code_rev": "${DMB_CODE_REV:-}"}
JSON
"$BASE_PY" -m harness.objstore put dmb-artefatos runtimes/rizzo_flow/cuda-latest.json /tmp/cuda-latest.json
cat /tmp/cuda-latest.json
echo "== ok"

37
infra/ds/gpu_probe.py Normal file
View File

@@ -0,0 +1,37 @@
"""Encontra um shape de GPU com capacidade: roda o job em cada shape, em ordem, até um não falhar
por falta de capacidade. Imprime o shape usado e o estado final.
Uso: PYTHONPATH=infra/oci:infra/ds .venv/bin/python infra/ds/gpu_probe.py <candidato> [shape ...]
"""
import sys
import jobs
DEFAULT_SHAPES = ["VM.GPU.A10.1", "VM.GPU.A10.2", "VM.GPU.A100.B40G.1", "VM.GPU.A100.80G.1", "VM.GPU3.1"]
def main():
args = sys.argv[1:]
region = None
if args and args[0].startswith("--region="):
region = args.pop(0).split("=", 1)[1]
candidate, shapes = args[0], args[1:] or DEFAULT_SHAPES
for shape in shapes:
try:
run_id = jobs.run_job(f"sanity-{candidate}-gpu", "infra/ds/run_job.sh", shape, None, None, region is None,
{"DMB_CANDIDATE": candidate, "DMB_MODE": "sanity", "DMB_DEVICE": "auto"},
storage_gb=60, region=region)
except jobs.oci.exceptions.ServiceError as error:
print(f"indisponível: {shape} ({error.code})", flush=True)
continue
r = jobs.wait(run_id, poll=20)
if "no capacity" in (r.lifecycle_details or "").lower():
print(f"sem capacidade: {shape}", flush=True)
continue
print(f"SHAPE {shape} -> {r.lifecycle_state} {run_id}", flush=True)
return
print("nenhum shape com capacidade", flush=True)
if __name__ == "__main__":
main()

47
infra/ds/gpu_queue.py Normal file
View File

@@ -0,0 +1,47 @@
"""Fila de jobs de GPU (o limite em sa-saopaulo-1 é de uma A10 por vez).
Uso: PYTHONPATH=infra/oci:infra/ds .venv/bin/python infra/ds/gpu_queue.py <modo> <candidato> [...] \
[--after <job_run_ocid>] [--shape VM.GPU.A10.1]
Grava "candidato run_ocid estado" em runs/gpu-queue.log.
"""
import argparse
import time
from pathlib import Path
import jobs
import provision
LOG = Path(__file__).resolve().parents[2] / "runs" / "gpu-queue.log"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("mode")
ap.add_argument("candidates", nargs="+")
ap.add_argument("--after")
ap.add_argument("--shape", default="VM.GPU.A10.1")
ap.add_argument("--storage", type=int, default=100)
args = ap.parse_args()
LOG.parent.mkdir(exist_ok=True)
if args.after:
jobs.wait(args.after, poll=30)
for cand in args.candidates:
while True:
try:
run_id = jobs.run_job(f"{args.mode}-{cand}-gpu", "infra/ds/run_job.sh", args.shape, None, None,
False, {"DMB_CANDIDATE": cand, "DMB_MODE": args.mode, "DMB_DEVICE": "auto"},
storage_gb=args.storage, region=provision.GPU_REGION)
break
except jobs.oci.exceptions.ServiceError as error:
if error.code != "LimitExceeded":
raise
time.sleep(60) # outra GPU ainda ocupada
r = jobs.wait(run_id, poll=30)
line = f"{cand} {run_id} {r.lifecycle_state} {(r.lifecycle_details or '')[:80]}"
print(line, flush=True)
with LOG.open("a") as f:
f.write(line + "\n")
if __name__ == "__main__":
main()

187
infra/ds/jobs.py Normal file
View File

@@ -0,0 +1,187 @@
"""Execução no OCI Data Science: a máquina local só empacota o código e dispara jobs.
Uso:
PYTHONPATH=infra/oci .venv/bin/python infra/ds/jobs.py run --name probe --entrypoint infra/ds/probe.sh \
[--shape VM.Standard.E4.Flex --ocpus 2 --memory 16 | --shape VM.GPU.A10.1] [--private] \
[--env K=V ...] [--wait]
PYTHONPATH=infra/oci .venv/bin/python infra/ds/jobs.py logs <job_run_ocid>
--private usa a sub-rede dmb-subnet-jobs (sem internet; só Service Gateway).
Sem --private, o job usa a rede gerenciada do serviço (com internet), só para builds.
"""
from __future__ import annotations
import argparse
import io
import subprocess
import sys
import time
import zipfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
import oci
from session import load_config
import provision
ROOT = Path(__file__).resolve().parents[2]
EXCLUDE_DIRS = {".git", ".secrets", "models", "reports", "runs", "__pycache__", ".pytest_cache"}
LOG_GROUP = "dmb-logs"
def compartment(cfg_home) -> str:
return provision.find_compartment(oci.identity.IdentityClient(cfg_home), cfg_home["tenancy"])
def code_zip() -> tuple[bytes, str]:
"""Snapshot do repositório local (sem segredos, modelos e venvs)."""
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr(zipfile.ZipInfo("dmb/"), "")
for p in sorted(ROOT.rglob("*")):
rel = p.relative_to(ROOT)
if any(part in EXCLUDE_DIRS or part.startswith(".venv") for part in rel.parts):
continue
if p.is_dir():
z.writestr(zipfile.ZipInfo(f"dmb/{rel.as_posix()}/"), "")
continue
info = zipfile.ZipInfo.from_file(p, f"dmb/{rel.as_posix()}")
info.compress_type = zipfile.ZIP_DEFLATED
z.writestr(info, p.read_bytes())
try:
rev = subprocess.run(["git", "-C", str(ROOT), "rev-parse", "--short", "HEAD"],
capture_output=True, text=True).stdout.strip() or "sem-commit"
except OSError:
rev = "sem-commit"
return buf.getvalue(), rev
def ensure_log_group(cfg, cid) -> str:
lc = oci.logging.LoggingManagementClient(cfg)
lg = next((g for g in lc.list_log_groups(cid, display_name=LOG_GROUP).data), None)
if lg is None:
lc.create_log_group(oci.logging.models.CreateLogGroupDetails(
compartment_id=cid, display_name=LOG_GROUP, description="Logs dos jobs do benchmark",
freeform_tags=provision.TAGS))
for _ in range(30):
lg = next((g for g in lc.list_log_groups(cid, display_name=LOG_GROUP).data), None)
if lg:
break
time.sleep(5)
return lg.id
def subnet_id(cfg, cid) -> str:
vn = oci.core.VirtualNetworkClient(cfg)
return next(s.id for s in vn.list_subnets(cid, display_name="dmb-subnet-jobs").data
if s.lifecycle_state == "AVAILABLE")
def run_job(name, entrypoint, shape, ocpus, memory, private, env, storage_gb=100, max_minutes=240, region=None):
cfg, home = load_config(region=region) if region else load_config(), load_config(region="us-ashburn-1")
cid = compartment(home)
ds = oci.data_science.DataScienceClient(cfg)
project = next(p.id for p in ds.list_projects(cid, lifecycle_state="ACTIVE").data
if p.display_name == "benchmark-decisoes-ptbr")
log_group = ensure_log_group(cfg, cid)
flex = shape.endswith(".Flex")
infra = oci.data_science.models.StandaloneJobInfrastructureConfigurationDetails(
job_infrastructure_type="STANDALONE" if private else "ME_STANDALONE",
shape_name=shape, block_storage_size_in_gbs=storage_gb,
subnet_id=subnet_id(cfg, cid) if private else None,
job_shape_config_details=oci.data_science.models.JobShapeConfigDetails(
ocpus=ocpus, memory_in_gbs=memory) if flex else None)
payload, rev = code_zip()
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
env = {"JOB_RUN_ENTRYPOINT": f"dmb/{entrypoint}", "DMB_CODE_REV": rev, **env}
if cfg["region"] == provision.GPU_REGION:
env.setdefault("DMB_BUCKET_ARTIFACTS", f"dmb-artefatos{provision.GPU_SUFFIX}")
env.setdefault("DMB_BUCKET_RESULTS", f"dmb-resultados{provision.GPU_SUFFIX}")
if not private:
env.setdefault("DMB_ISOLATION", "app")
job = ds.create_job(oci.data_science.models.CreateJobDetails(
project_id=project, compartment_id=cid, display_name=f"dmb-{name}-{stamp}",
freeform_tags=provision.TAGS,
job_configuration_details=oci.data_science.models.DefaultJobConfigurationDetails(
job_type="DEFAULT", environment_variables=env,
maximum_runtime_in_minutes=max_minutes),
job_infrastructure_configuration_details=infra,
job_log_configuration_details=oci.data_science.models.JobLogConfigurationDetails(
enable_logging=True, enable_auto_log_creation=True, log_group_id=log_group))).data
ds.create_job_artifact(job.id, payload, content_disposition="attachment; filename=dmb.zip")
run = ds.create_job_run(oci.data_science.models.CreateJobRunDetails(
project_id=project, compartment_id=cid, job_id=job.id,
display_name=f"dmb-{name}-{stamp}", freeform_tags=provision.TAGS)).data
print(f"job {job.id}\nrun {run.id}", flush=True)
return run.id
def region_of(ocid: str) -> str:
return ocid.split(".")[3] if ocid.count(".") >= 4 and ocid.split(".")[3] else load_config()["region"]
def wait(run_id, poll=30):
ds = oci.data_science.DataScienceClient(load_config(region=region_of(run_id)))
while True:
r = ds.get_job_run(run_id).data
if r.lifecycle_state in ("SUCCEEDED", "FAILED", "CANCELED", "DELETED"):
print("estado:", r.lifecycle_state, r.lifecycle_details or "", flush=True)
return r
time.sleep(poll)
def logs(run_id, limit=500):
cfg = load_config(region=region_of(run_id))
ds = oci.data_science.DataScienceClient(cfg)
r = ds.get_job_run(run_id).data
if not r.log_details:
print("sem log configurado ainda")
return
search = oci.loggingsearch.LogSearchClient(cfg)
start = r.time_accepted - timedelta(minutes=5)
end = (r.time_finished or datetime.now(timezone.utc)) + timedelta(minutes=10)
query = (f'search "{r.compartment_id}/{r.log_details.log_group_id}/{r.log_details.log_id}" '
"| sort by datetime asc")
try:
res = search.search_logs(oci.loggingsearch.models.SearchLogsDetails(
time_start=start, time_end=end, search_query=query, is_return_field_info=False), limit=limit).data
except oci.exceptions.ServiceError as error:
print(f"logs indisponíveis ({error.status} {error.code}); log_id={r.log_details.log_id}")
return
for item in res.results or []:
print(item.data.get("logContent", {}).get("data", {}).get("message", ""))
def main():
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
r = sub.add_parser("run")
r.add_argument("--name", required=True)
r.add_argument("--entrypoint", required=True)
r.add_argument("--shape", default="VM.Standard.E4.Flex")
r.add_argument("--ocpus", type=float, default=2)
r.add_argument("--memory", type=float, default=16)
r.add_argument("--storage", type=int, default=100)
r.add_argument("--private", action="store_true")
r.add_argument("--env", nargs="*", default=[])
r.add_argument("--wait", action="store_true")
r.add_argument("--region", help="padrão: us-chicago-1; sa-saopaulo-1 para GPU")
lg = sub.add_parser("logs")
lg.add_argument("run_id")
args = ap.parse_args()
if args.cmd == "logs":
logs(args.run_id)
return
env = dict(kv.split("=", 1) for kv in args.env)
run_id = run_job(args.name, args.entrypoint, args.shape, args.ocpus, args.memory, args.private,
env, storage_gb=args.storage, region=args.region)
if args.wait:
result = wait(run_id)
time.sleep(60) # ingestão dos logs
logs(run_id)
sys.exit(0 if result.lifecycle_state == "SUCCEEDED" else 1)
if __name__ == "__main__":
main()

11
infra/ds/probe.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Sonda do ambiente padrão dos jobs do Data Science.
set -x
whoami; echo "HOME=$HOME PWD=$PWD"; cat /etc/os-release | head -3; uname -m
which python3 python conda curl tar unzip git 2>&1
python3 --version 2>&1; python3 -c "import oci; print('oci', oci.__version__)" 2>&1
df -h $HOME /tmp 2>&1 | tail -3; nproc; free -g | head -2
nvidia-smi 2>&1 | head -5
env | grep -E '^(JOB_|OCI_|CONDA|DMB_|NB_)' | sed -E 's/(TOKEN|KEY|SECRET)=.*/\1=***/' | sort
ls "$PWD" | head; ls /home/datascience 2>&1 | head
timeout 5 curl -sI https://pypi.org | head -1 || echo "sem internet"

38
infra/ds/results.py Normal file
View File

@@ -0,0 +1,38 @@
"""Resumo das execuções gravadas em dmb-resultados/runs/.
Uso: PYTHONPATH=infra/oci .venv/bin/python infra/ds/results.py [filtro]
"""
import json
import sys
import oci
from session import load_config
BUCKET = "dmb-resultados"
def main():
needle = sys.argv[1] if len(sys.argv) > 1 else ""
c = oci.object_storage.ObjectStorageClient(load_config())
ns = c.get_namespace().data
names = [o.name for o in oci.pagination.list_call_get_all_results(
c.list_objects, ns, BUCKET, prefix="runs/").data.objects]
for name in sorted(n for n in names if n.endswith("/run.json") and needle in n):
run = json.loads(c.get_object(ns, BUCKET, name).data.content)
line = {k: run.get(k) for k in ("run_id", "candidate", "device", "status")}
line["gate"] = run.get("egress_gate", {}).get("passed")
prefix = name.rsplit("/", 1)[0]
for n in names:
if n.startswith(prefix) and n.endswith(".json") and n != name:
rep = json.loads(c.get_object(ns, BUCKET, n).data.content)
if "summary" in rep:
line |= rep["summary"] | {"load_s": rep.get("load_seconds")}
lat = [r["latency_ms"] for r in rep.get("sanity", [])]
if lat:
line["latency_ms_median"] = sorted(lat)[len(lat) // 2]
print(json.dumps(line, ensure_ascii=False))
if __name__ == "__main__":
main()

6
infra/ds/run_job.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Entrada dos jobs de benchmark: usa o Python base do job (SDK OCI + resource principal)
# para o processo pai; a inferência roda no ambiente do candidato (ver harness/job.py).
set -euo pipefail
cd "$(dirname "$0")/../.."
exec /opt/conda/bin/python3 -m harness.job

View File

@@ -1,8 +1,13 @@
"""Provisionamento idempotente da fase F0 (tarefas 1, 2, 5, 6, 7, 8).
"""Provisionamento idempotente da fase F0 (tarefas 1, 2, 5, 6, 7, 8) e do ambiente de execução.
Ambiente de execução: a máquina local é só IDE e repositório. Os scripts rodam na VM
dmb-vm (sub-rede de desenvolvimento, SSH só do IP autorizado) e em notebooks e jobs do
OCI Data Science. A sub-rede privada dmb-subnet-jobs continua sem saída para a internet.
Uso: PYTHONPATH=infra/oci .venv/bin/python infra/oci/provision.py
Registra os artefatos criados em .secrets/artefatos-OCI.md.
"""
import os
import time
from datetime import datetime, timezone
@@ -16,7 +21,19 @@ TAGS = {"projeto": "benchmark-decisoes-ptbr", "ambiente": "benchmark"} # centro
BUCKETS = ["entrada", "artefatos", "resultados", "logs-imutaveis"]
VCN_CIDR = "10.20.0.0/16"
SUBNET_CIDR = "10.20.1.0/24"
DEV_SUBNET_CIDR = "10.20.2.0/24"
SSH_ALLOWED_CIDR = os.environ.get("DMB_SSH_CIDR", "203.0.113.1/32")
# VM de trabalho criada pelo usuário no console (o lançamento via API é negado neste compartment).
VM_NAME = "instance-20261007-1206"
VM_SSH_KEY = ".secrets/ssh/ssh-key-oci.key"
VM_USER = "opc"
NOTEBOOK_NAME = "dmb-notebook-cpu"
NOTEBOOK_SHAPE = ("VM.Standard.E4.Flex", 4, 32)
ARTIFACTS_FILE = CRED_DIR.parent / "artefatos-OCI.md"
# Região secundária só para jobs de GPU (A10 sem capacidade em us-chicago-1).
GPU_REGION = "sa-saopaulo-1"
GPU_SUFFIX = "-gru"
GPU_BUCKETS = ["artefatos", "resultados"]
artifacts = []
@@ -64,21 +81,22 @@ def ensure_ds_project(cfg, cid):
proj = ds.create_project(oci.data_science.models.CreateProjectDetails(
compartment_id=cid, display_name=name, freeform_tags=TAGS,
description="Benchmark de modelos de decisão PT-BR")).data
record("Data Science Project", name, proj.id, REGION)
record("Data Science Project", name, proj.id, cfg["region"])
return proj.id
def ensure_buckets(cfg, cid):
def ensure_buckets(cfg, cid, suffix="", buckets=BUCKETS):
os_ = oci.object_storage.ObjectStorageClient(cfg)
ns = os_.get_namespace().data
existing = {b.name for b in oci.pagination.list_call_get_all_results(os_.list_buckets, ns, cid).data}
for short in BUCKETS:
name = f"dmb-{short}"
for short in buckets:
name = f"dmb-{short}{suffix}"
if name not in existing:
os_.create_bucket(ns, oci.object_storage.models.CreateBucketDetails(
name=name, compartment_id=cid, public_access_type="NoPublicAccess",
storage_tier="Standard", versioning="Enabled", freeform_tags=TAGS))
b = os_.get_bucket(ns, name).data
record("Object Storage Bucket", f"{name} (namespace {ns})", b.id, REGION)
record("Object Storage Bucket", f"{name} (namespace {ns})", b.id, cfg["region"])
def ensure_network(cfg, cid):
@@ -92,7 +110,7 @@ def ensure_network(cfg, cid):
compartment_id=cid, display_name="dmb-vcn", cidr_blocks=[VCN_CIDR],
dns_label="dmbvcn", freeform_tags=TAGS)).data
vcn = comp(vn, vn.get_vcn(vcn.id), "lifecycle_state", "AVAILABLE").data
record("VCN", f"dmb-vcn ({VCN_CIDR})", vcn.id, REGION)
record("VCN", f"dmb-vcn ({VCN_CIDR})", vcn.id, cfg["region"])
svc = next(s for s in vn.list_services().data if s.cidr_block.startswith("all-") and "services-in-oracle-services-network" in s.cidr_block)
sgw = next((g for g in vn.list_service_gateways(cid, vcn_id=vcn.id).data
@@ -102,7 +120,7 @@ def ensure_network(cfg, cid):
compartment_id=cid, vcn_id=vcn.id, display_name="dmb-sgw", freeform_tags=TAGS,
services=[oci.core.models.ServiceIdRequestDetails(service_id=svc.id)])).data
sgw = comp(vn, vn.get_service_gateway(sgw.id), "lifecycle_state", "AVAILABLE").data
record("Service Gateway", f"dmb-sgw ({svc.cidr_block})", sgw.id, REGION)
record("Service Gateway", f"dmb-sgw ({svc.cidr_block})", sgw.id, cfg["region"])
rt = next((r for r in vn.list_route_tables(cid, vcn_id=vcn.id, display_name="dmb-rt-privada").data
if r.lifecycle_state == "AVAILABLE"), None)
@@ -113,7 +131,7 @@ def ensure_network(cfg, cid):
destination=svc.cidr_block, destination_type="SERVICE_CIDR_BLOCK",
network_entity_id=sgw.id, description="Object Storage e serviços OCI via Service Gateway")])).data
rt = comp(vn, vn.get_route_table(rt.id), "lifecycle_state", "AVAILABLE").data
record("Route Table", "dmb-rt-privada (somente Service Gateway)", rt.id, REGION)
record("Route Table", "dmb-rt-privada (somente Service Gateway)", rt.id, cfg["region"])
sl = next((s for s in vn.list_security_lists(cid, vcn_id=vcn.id, display_name="dmb-sl-privada").data
if s.lifecycle_state == "AVAILABLE"), None)
@@ -131,7 +149,7 @@ def ensure_network(cfg, cid):
oci.core.models.IngressSecurityRule(source=VCN_CIDR, protocol="all", description="Tráfego interno da VCN"),
])).data
sl = comp(vn, vn.get_security_list(sl.id), "lifecycle_state", "AVAILABLE").data
record("Security List", "dmb-sl-privada (egresso só para serviços OCI e VCN)", sl.id, REGION)
record("Security List", "dmb-sl-privada (egresso só para serviços OCI e VCN)", sl.id, cfg["region"])
sub = next((s for s in vn.list_subnets(cid, vcn_id=vcn.id, display_name="dmb-subnet-jobs").data
if s.lifecycle_state == "AVAILABLE"), None)
@@ -141,13 +159,14 @@ def ensure_network(cfg, cid):
dns_label="jobs", prohibit_public_ip_on_vnic=True, route_table_id=rt.id,
security_list_ids=[sl.id], freeform_tags=TAGS)).data
sub = comp(vn, vn.get_subnet(sub.id), "lifecycle_state", "AVAILABLE").data
record("Subnet (privada)", f"dmb-subnet-jobs ({SUBNET_CIDR})", sub.id, REGION)
record("Subnet (privada)", f"dmb-subnet-jobs ({SUBNET_CIDR})", sub.id, cfg["region"])
# O NAT é criado só por nat.py; aqui ele só é registrado, se existir.
for nat in vn.list_nat_gateways(cid, vcn_id=vcn.id).data:
if nat.lifecycle_state == "AVAILABLE":
status = "bloqueado" if nat.block_traffic else "ATIVO"
record("NAT Gateway (temporário)", f"{nat.display_name} ({status})", nat.id, REGION)
record("NAT Gateway (temporário)", f"{nat.display_name} ({status})", nat.id, cfg["region"])
return vcn.id
USER_FAMILIES = [
@@ -157,6 +176,9 @@ USER_FAMILIES = [
"logging-family",
"repos",
"generative-ai-family",
"instance-family",
"volume-family",
"instance-agent-command-family",
]
@@ -192,18 +214,130 @@ def ensure_policies(idn, cid, user_id):
f"Allow any-user to use log-content in {comp} where all {{{job}}}",
f"Allow any-user to read repos in {comp} where all {{{job}}}",
f"Allow service datascience to use virtual-network-family in {comp}",
# copy_object entre regiões (réplica para a região de GPU)
f"Allow service objectstorage-{REGION} to manage object-family in {comp}",
f"Allow service objectstorage-{GPU_REGION} to manage object-family in {comp}",
]
ensure_policy(idn, cid, "dmb-jobs-menor-privilegio",
"Jobs de benchmark: leitura de entrada/artefatos e escrita de resultados/logs (tarefa 7)",
job_stmts)
def ensure_workload_policy(idn, cid):
"""VM (instance principal) e notebooks do Data Science (resource principal), sem dynamic groups."""
comp = f"compartment id {cid}"
who = (f"all {{request.principal.compartment.id = '{cid}', "
"any {request.principal.type = 'instance', request.principal.type = 'datasciencenotebooksession'}}")
stmts = [f"Allow any-user to {verb} in {comp} where {who}" for verb in (
"read buckets",
"manage objects",
"manage data-science-family",
"manage repos",
"use virtual-network-family",
"use log-groups",
"use log-content",
"use generative-ai-family",
"read compartments",
)]
ensure_policy(idn, cid, "dmb-workloads", "VM de trabalho e notebooks do benchmark", stmts)
def ensure_dev_network(vn, cid, vcn_id):
cfg = {"region": REGION} # a sub-rede de desenvolvimento só existe na região principal
igw = next((g for g in vn.list_internet_gateways(cid, vcn_id=vcn_id).data
if g.lifecycle_state == "AVAILABLE"), None)
if igw is None:
igw = vn.create_internet_gateway(oci.core.models.CreateInternetGatewayDetails(
compartment_id=cid, vcn_id=vcn_id, display_name="dmb-igw-dev", is_enabled=True,
freeform_tags=TAGS)).data
igw = oci.wait_until(vn, vn.get_internet_gateway(igw.id), "lifecycle_state", "AVAILABLE").data
record("Internet Gateway", "dmb-igw-dev (só para a sub-rede de desenvolvimento)", igw.id, cfg["region"])
rt = next((r for r in vn.list_route_tables(cid, vcn_id=vcn_id, display_name="dmb-rt-dev").data
if r.lifecycle_state == "AVAILABLE"), None)
if rt is None:
rt = vn.create_route_table(oci.core.models.CreateRouteTableDetails(
compartment_id=cid, vcn_id=vcn_id, display_name="dmb-rt-dev", freeform_tags=TAGS,
route_rules=[oci.core.models.RouteRule(destination="0.0.0.0/0", destination_type="CIDR_BLOCK",
network_entity_id=igw.id)])).data
rt = oci.wait_until(vn, vn.get_route_table(rt.id), "lifecycle_state", "AVAILABLE").data
record("Route Table", "dmb-rt-dev (internet via IGW)", rt.id, cfg["region"])
ingress = [oci.core.models.IngressSecurityRule(
source=SSH_ALLOWED_CIDR, protocol="6", description="SSH só do IP autorizado",
tcp_options=oci.core.models.TcpOptions(destination_port_range=oci.core.models.PortRange(min=22, max=22)))]
sl = next((x for x in vn.list_security_lists(cid, vcn_id=vcn_id, display_name="dmb-sl-dev").data
if x.lifecycle_state == "AVAILABLE"), None)
if sl is None:
sl = vn.create_security_list(oci.core.models.CreateSecurityListDetails(
compartment_id=cid, vcn_id=vcn_id, display_name="dmb-sl-dev", freeform_tags=TAGS,
ingress_security_rules=ingress,
egress_security_rules=[oci.core.models.EgressSecurityRule(destination="0.0.0.0/0", protocol="all")])).data
sl = oci.wait_until(vn, vn.get_security_list(sl.id), "lifecycle_state", "AVAILABLE").data
elif [r.source for r in sl.ingress_security_rules] != [SSH_ALLOWED_CIDR]:
vn.update_security_list(sl.id, oci.core.models.UpdateSecurityListDetails(
ingress_security_rules=ingress, egress_security_rules=sl.egress_security_rules))
record("Security List", f"dmb-sl-dev (SSH de {SSH_ALLOWED_CIDR})", sl.id, cfg["region"])
sub = next((x for x in vn.list_subnets(cid, vcn_id=vcn_id, display_name="dmb-subnet-dev").data
if x.lifecycle_state == "AVAILABLE"), None)
if sub is None:
sub = vn.create_subnet(oci.core.models.CreateSubnetDetails(
compartment_id=cid, vcn_id=vcn_id, display_name="dmb-subnet-dev", cidr_block=DEV_SUBNET_CIDR,
dns_label="dev", prohibit_public_ip_on_vnic=False, route_table_id=rt.id,
security_list_ids=[sl.id], freeform_tags=TAGS)).data
sub = oci.wait_until(vn, vn.get_subnet(sub.id), "lifecycle_state", "AVAILABLE").data
record("Subnet (desenvolvimento)", f"dmb-subnet-dev ({DEV_SUBNET_CIDR})", sub.id, cfg["region"])
return sub.id
def ensure_vm(cfg, cid):
"""Registra a VM de trabalho, os volumes anexados e o acesso SSH. Não cria instâncias."""
cc = oci.core.ComputeClient(cfg)
vm = next((i for i in cc.list_instances(cid, display_name=VM_NAME).data
if i.lifecycle_state not in ("TERMINATED", "TERMINATING")), None)
if vm is None:
print(f"aviso: VM {VM_NAME} não encontrada")
return None
vn = oci.core.VirtualNetworkClient(cfg)
bs = oci.core.BlockstorageClient(cfg)
vnic = vn.get_vnic(cc.list_vnic_attachments(cid, instance_id=vm.id).data[0].vnic_id).data
record("Compute Instance", f"{VM_NAME} ({vm.shape}, {vm.shape_config.ocpus:.0f} OCPU, "
f"{vm.shape_config.memory_in_gbs:.0f} GB, Oracle Linux 9, IP público {vnic.public_ip}, "
f"usuário {VM_USER}, chave {VM_SSH_KEY})", vm.id, cfg["region"])
for att in cc.list_boot_volume_attachments(vm.availability_domain, cid, instance_id=vm.id).data:
bv = bs.get_boot_volume(att.boot_volume_id).data
record("Boot Volume", f"{bv.display_name} ({bv.size_in_gbs} GB, raiz /)", bv.id, cfg["region"])
for att in cc.list_volume_attachments(cid, instance_id=vm.id).data:
if att.lifecycle_state == "ATTACHED":
v = bs.get_volume(att.volume_id).data
record("Block Volume", f"{v.display_name} ({v.size_in_gbs} GB, {att.attachment_type}, "
f"{att.device}, montado em /data)", v.id, cfg["region"])
return vnic.public_ip
def ensure_notebook(cfg, cid, project_id):
ds = oci.data_science.DataScienceClient(cfg)
nb = next((n for n in ds.list_notebook_sessions(cid, project_id=project_id).data
if n.display_name == NOTEBOOK_NAME and n.lifecycle_state not in ("DELETED", "DELETING")), None)
if nb is None:
shape, ocpus, mem = NOTEBOOK_SHAPE
nb = ds.create_notebook_session(oci.data_science.models.CreateNotebookSessionDetails(
compartment_id=cid, project_id=project_id, display_name=NOTEBOOK_NAME, freeform_tags=TAGS,
notebook_session_config_details=oci.data_science.models.NotebookSessionConfigDetails(
shape=shape, block_storage_size_in_gbs=200,
notebook_session_shape_config_details=oci.data_science.models.NotebookSessionShapeConfigDetails(
ocpus=ocpus, memory_in_gbs=mem)))).data
record("Data Science Notebook Session", f"{NOTEBOOK_NAME} ({NOTEBOOK_SHAPE[0]}, rede gerenciada, "
f"estado {nb.lifecycle_state})", nb.id, cfg["region"])
def write_artifacts():
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
lines = [
"# Artefatos OCI — benchmark de modelos de decisão",
"",
f"Atualizado em {now} por `infra/oci/provision.py`. Região dos recursos: `{REGION}`.",
f"Atualizado em {now} por `infra/oci/provision.py`. Região principal: `{REGION}`; GPU também em `{GPU_REGION}`.",
f"Tags livres aplicadas: {', '.join(f'`{k}={v}`' for k, v in TAGS.items())} (`centro_custo` pendente, Q13).",
"",
"| Tipo | Nome | OCID | Região |",
@@ -219,9 +353,24 @@ def main():
idn = oci.identity.IdentityClient(home)
cid = find_compartment(idn, home["tenancy"])
ensure_policies(idn, cid, home["user"])
ensure_ds_project(cfg, cid)
ensure_workload_policy(idn, cid)
project_id = ensure_ds_project(cfg, cid)
ensure_buckets(cfg, cid)
ensure_network(cfg, cid)
vcn_id = ensure_network(cfg, cid)
ensure_dev_network(oci.core.VirtualNetworkClient(cfg), cid, vcn_id)
ensure_vm(cfg, cid)
ensure_notebook(cfg, cid, project_id)
gru = load_config(region=GPU_REGION)
ensure_ds_project(gru, cid)
ensure_buckets(gru, cid, suffix=GPU_SUFFIX, buckets=GPU_BUCKETS)
try:
ensure_network(gru, cid)
except oci.exceptions.ServiceError as error:
if error.code != "LimitExceeded":
raise
# Sem VCN disponível na região: jobs de GPU usam rede gerenciada + isolamento no processo.
print(f"aviso: VCN indisponível em {GPU_REGION} ({error.message[:80]})")
write_artifacts()
for a in artifacts:
print(" | ".join(a))

63
infra/oci/replicate.py Normal file
View File

@@ -0,0 +1,63 @@
"""Réplica server-side dos artefatos para a região de GPU (copy_object entre regiões).
Copia os pesos (models/) e o ambiente atual de cada candidato (envs/<c>/latest.json e o
pacote que ele referencia). Objetos já presentes com o mesmo tamanho são pulados.
Uso: PYTHONPATH=infra/oci .venv/bin/python infra/oci/replicate.py [--wait]
"""
import json
import sys
import time
import oci
import provision
from session import load_config
SRC_BUCKET = "dmb-artefatos"
DST_BUCKET = f"dmb-artefatos{provision.GPU_SUFFIX}"
def main():
src = oci.object_storage.ObjectStorageClient(load_config())
dst = oci.object_storage.ObjectStorageClient(load_config(region=provision.GPU_REGION))
ns = src.get_namespace().data
objects = oci.pagination.list_call_get_all_results(
src.list_objects, ns, SRC_BUCKET, fields="size").data.objects
sizes = {o.name: o.size for o in objects}
wanted = [n for n in sizes if n.startswith("models/")]
for cand in ("laya", "gliner", "semif", "rizzo_flow"):
latest = f"envs/{cand}/latest.json"
if latest in sizes:
obj = json.loads(src.get_object(ns, SRC_BUCKET, latest).data.content)["object"]
wanted += [latest, obj, obj.replace(".tar.gz", "-env-manifest.json")]
cuda = "runtimes/rizzo_flow/cuda-latest.json"
if cuda in sizes:
wanted += [cuda, json.loads(src.get_object(ns, SRC_BUCKET, cuda).data.content)["object"]]
existing = {o.name: o.size for o in oci.pagination.list_call_get_all_results(
dst.list_objects, ns, DST_BUCKET, fields="size").data.objects}
requests = []
for name in wanted:
if name not in sizes:
continue
if existing.get(name) == sizes[name] and not name.endswith("latest.json"):
continue
r = src.copy_object(ns, SRC_BUCKET, oci.object_storage.models.CopyObjectDetails(
source_object_name=name, destination_region=provision.GPU_REGION,
destination_namespace=ns, destination_bucket=DST_BUCKET, destination_object_name=name))
requests.append((name, r.headers["opc-work-request-id"]))
total = sum(sizes[n] for n, _ in requests)
print(f"{len(requests)} cópias iniciadas ({total / 1e9:.2f} GB) para {DST_BUCKET} em {provision.GPU_REGION}")
if "--wait" in sys.argv:
pending = dict(requests)
while pending:
for name, wr in list(pending.items()):
status = src.get_work_request(wr).data.status
if status in ("COMPLETED", "FAILED", "CANCELED"):
print(status, name, flush=True)
pending.pop(name)
time.sleep(20)
if __name__ == "__main__":
main()

14
infra/vm/bootstrap.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Preparação idempotente da VM de trabalho (Oracle Linux 9). Rodar na VM:
# bash infra/vm/bootstrap.sh
set -euo pipefail
sudo dnf install -y -q git rsync tmux jq python3.12 python3.12-pip python3.12-devel gcc gcc-c++ >/dev/null
[ -d "$HOME/.venv-oci" ] || python3.12 -m venv "$HOME/.venv-oci"
"$HOME/.venv-oci/bin/pip" install -q --upgrade pip
"$HOME/.venv-oci/bin/pip" install -q "oci==2.187.2" oci-cli "huggingface_hub==0.36.2" jsonschema fastapi uvicorn pyyaml pytest httpx
grep -q OCI_CLI_AUTH "$HOME/.bashrc" || cat >> "$HOME/.bashrc" <<'RC'
export OCI_CLI_AUTH=instance_principal
export DMB_OCI_AUTH=instance_principal
export PATH=$HOME/.venv-oci/bin:$PATH
RC
echo "bootstrap ok: $(python3.12 --version), oci $("$HOME/.venv-oci/bin/python" -c 'import oci;print(oci.__version__)')"

26
infra/vm/remote.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Máquina local = IDE + repositório; execução na VM dmb-vm.
# infra/vm/remote.sh sync # envia o repositório (sem .secrets, models, venvs)
# infra/vm/remote.sh run <comando...> # sincroniza e roda o comando na VM, dentro do repositório
# infra/vm/remote.sh ssh # abre um shell na VM
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
KEY="$ROOT/.secrets/ssh/ssh-key-oci.key"
IP="$(grep -oE 'IP público [0-9.]+' "$ROOT/.secrets/artefatos-OCI.md" | head -1 | awk '{print $3}')"
[ -n "$IP" ] || { echo "IP da VM não encontrado em .secrets/artefatos-OCI.md" >&2; exit 1; }
SSH=(ssh -i "$KEY" -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 "opc@$IP")
REMOTE_DIR=/data/repo
sync() {
rsync -az --delete -e "ssh -i $KEY -o StrictHostKeyChecking=accept-new" \
--exclude '.git/' --exclude '.secrets/' --exclude 'models/' --exclude '.venv*/' \
--exclude '__pycache__/' --exclude 'reports/sanity/' --exclude 'runs/' \
"$ROOT/" "opc@$IP:$REMOTE_DIR/"
}
case "${1:-}" in
sync) sync ;;
run) shift; sync; "${SSH[@]}" "cd $REMOTE_DIR && bash -lc $(printf '%q' "$*")" ;;
ssh) "${SSH[@]}" ;;
*) sed -n '2,6p' "$0"; exit 1 ;;
esac