aiauto-client 0.2.8
AI Auto HPO (Hyperparameter Optimization) Client Library
Requires Python: >=3.8.1

# AIAuto Client

> Kubernetes 기반 분산 하이퍼파라미터 최적화(HPO) 라이브러리

AIAuto는 기존 Optuna API와 호환되는 Kubernetes 기반 분산 HPO 플랫폼입니다. 로컬 PC에서 코드를 작성하면 ZeroOneAI의 Kubernetes 클러스터에서 병렬 최적화가 자동으로 실행됩니다. GPU도 지원합니다.

---

## 1. 설치

- 요구사항**: Python 3.8 이상
```bash
pip install aiauto-client optuna
```

### 환경변수 설정 (선택)

기본 API 서버(`https://aiauto.pangyo.ainode.ai:443`)가 아닌 다른 서버에 연결하려면 `AIAUTO_BASE_URL` 환경변수를 설정합니다.

```bash
# 방법 1: 명령어 앞에 환경변수 지정 (일회성)
AIAUTO_BASE_URL=https://aiauto.dev.example.com:443 uv run python test.py
AIAUTO_BASE_URL=https://aiauto.dev.example.com:443 python test.py

# 방법 2: shell에서 export (현재 세션 동안 유지)
export AIAUTO_BASE_URL=https://aiauto.dev.example.com:443
uv run python test.py

# IP 기반 배포의 경우
export AIAUTO_BASE_URL=https://192.168.1.100:443
export AIAUTO_BASE_URL=https://192.168.1.100:8080  # 비표준 포트
```

**URL 형식**: `scheme://host:port` (포트 필수)
- `scheme`: `http` 또는 `https`
- `port`: 필수 (예: `:443`, `:80`, `:8080`)

**Self-signed 인증서 사용 시** (내부망/개발 환경):
```bash
# SSL 인증서 검증 비활성화
export AIAUTO_INSECURE=true
export AIAUTO_BASE_URL=https://192.168.1.100:80
uv run python test.py

# 또는 한 줄로
AIAUTO_INSECURE=true AIAUTO_BASE_URL=https://192.168.1.100:80 uv run python test.py
```

> **주의**: 환경변수는 Python 프로세스 시작 시 한 번만 읽힙니다. 런타임 중 변경해도 이미 생성된 `AIAutoController` 인스턴스에는 반영되지 않습니다.

---

## 2. 주요 개념

### 2-1. [Optuna](https://optuna.org/) vs AIAuto 차이점
#### 2-1-1. AIAuto 를 사용하는 이유
- 로컬 리소스 제약 해결: 로컬 PC 의 CPU/GPU/메모리 한계 없이 ZeroOneAI 클러스터의 자원을 활용한 대규모 하이퍼파라미터 탐색
- 클라우드 GPU 활용: 로컬에서 돌아가는 것이 아닌 ZeroOneAI 클러스터의 GPU 사용
- Optuna 호환: 기존 Optuna 코드와 Sampler/Pruner 객체를 그대로 사용 가능

| 항목 | Optuna        | AIAuto                |
|------|---------------|-----------------------|
| **실행 위치** | 로컬 PC         | Kubernetes 클러스터       |
| **병렬화** | 프로세스/스레드 기반   | Kubernetes Pod 기반     |
| **GPU 사용** | 로컬 GPU 필요     | ZeroOneAI 클러스터 GPU 사용 |
| **확장성** | 로컬 리소스 확장 제한  | 노드 기반 scale out 확장 가능 |
| **API 호환성** | Optuna 기본 API | Optuna API 호환         |
| **Sampler/Pruner** | 모든 Optuna 알고리즘 | Optuna API 호환          |

### 2-2. 아키텍처 구조
```mermaid
graph TB
    subgraph Layer1["로컬 환경"]
        User[사용자 로컬 PC]
        AskTell["Ask/Tell trial"]
        
        User --> AskTell
    end

    subgraph Layer2["AIAuto 플랫폼"]
        Workspace[Workspace]
        Dashboard[Optuna Dashboard]
        Storage[Journal gRPC Storage]
        Store[Artifact Store]
        
        Study[Study]

        Workspace --> Dashboard
        Workspace --> Storage
        Workspace --> Store
        Workspace --> Study
    end

    subgraph Layer3["분산 실행"]
        TB1[TrialBatch 1]
        TB2[TrialBatch 2]
        Pod1[Pod/Trial 1]
        Pod2[Pod/Trial 2]

        TB1 --> Pod1
        TB1 --> Pod2
    end

    User --> Workspace
    
    Study --> TB1
    Study --> TB2
    
    Pod1 -.report.-> Storage
    Pod1 -.save.-> Store
    AskTell -.report.-> Storage
    AskTell -.save.-> User
    Storage -.view.-> Dashboard
```

#### 2-2-1. 구조 설명
- OptunaWorkspace: 사용자당 1개, Journal gRPC Storage 와 Optuna Dashboard, Artifact Store 포함
- Study: 하나의 maximize or minimize 최적화 실험 단위 (ex: "ResNet 하이퍼파라미터 탐색")
- TrialBatch: Study 내에서 병렬 실행되는 trial 그룹 (한 번의 `optimize()` 호출당 1개)
- Pod: Kubernetes 에서 실행되는 개별 trial (GPU/CPU 리소스 할당)

### 2-3. Trial Pod Volume 가이드

Trial Pod에서 사용할 수 있는 Volume은 3가지입니다.

#### 2-3-1. Volume 비교표

| Volume | 마운트 경로 | 타입 | 공유 범위 | 수명 | 주 용도 |
|--------|------------|------|----------|------|--------|
| tmp-cache | `{TmpCacheDir}` (기본: `/mnt/tmp-cache`) | emptyDir | Pod 1개 내부 | Pod 종료 시 삭제 | 임시 파일, 로컬 복사 |
| shared-cache | `{SharedCacheDir}` (기본: `/mnt/shared-cache`) | PVC | 모든 Trial | Workspace 수명 | 공유 데이터, 캐시 |
| artifact-store | `/artifacts` (고정) | PVC | 모든 Trial | Workspace 수명 | 결과물 저장 (top_n_artifacts) |

> **주의**: artifact-store (`/artifacts`)는 `top_n_artifacts` 자동 정리 로직이 동작합니다. 직접 파일을 쓰지 말고 `AIAutoController.upload_artifact()` API를 사용하세요.

#### 2-3-2. 권장 패턴: 학습 데이터 다운로드 (Trial #0 지정 방식)

**문제**: 첫 trial들이 동시에 뜰 수 있음 → 동시 다운로드 시 파일 손상

**해결**: Dashboard에서 shared-cache로 업로드하거나, Trial #0만 다운로드 후 나머지는 완료 파일 대기

```python
import os
import time
import shutil
from torchvision import datasets

def objective(trial):
    shared_path = "/mnt/shared-cache/cifar10"
    done_file = "/mnt/shared-cache/cifar10.done"
    local_path = "/mnt/tmp-cache/cifar10"

    # 1단계: Trial #0만 다운로드 (train + test 모두)
    if trial.number == 0:
        if not os.path.exists(done_file):
            datasets.CIFAR10(root=shared_path, train=True, download=True)
            datasets.CIFAR10(root=shared_path, train=False, download=True)
            # open()은 with 없이 사용 제한 (섹션 4-9 참조)
            with open(done_file, 'w'):
                pass  # 완료 표시
    else:
        # 나머지 trial은 완료 파일 대기
        while not os.path.exists(done_file):
            time.sleep(10)

    # 2단계: shared-cache → tmp-cache 복사 (빠른 로컬 I/O)
    if not os.path.exists(local_path):
        shutil.copytree(shared_path, local_path)

    # 3단계: tmp-cache에서 학습
    dataset = datasets.CIFAR10(root=local_path, download=False)
```

#### 2-3-3. tmp-cache (emptyDir)

**특징**: 노드 로컬 저장소, Pod 종료 시 자동 삭제

**저장소 옵션**:
- 기본: 노드 디스크 사용
- `use_tmp_cache_mem=True`: RAM 기반 emptyDir(tmpfs) 사용

**용도**:
- shared-cache 데이터를 로컬에 복사하여 빠른 I/O 확보
- 중간 계산 결과 임시 저장 (해당 Trial에서만 필요한 경우)
- 외부 API 호출 결과 캐싱
- objective 함수 내 반복문에서 재사용

#### 2-3-4. shared-cache (PVC)

**특징**: 모든 Trial Pod에서 공유, Workspace 삭제 전까지 유지

**용도**:
- 학습 데이터셋 (모든 Trial이 같은 데이터 사용)
- 전처리 결과 공유 (첫 Trial이 생성 → 나머지 Trial이 재사용)

#### 2-3-5. 상황별 Volume 선택 가이드

| 상황 | 추천 Volume | 이유 |
|------|-------------|-----|
| 외부 데이터 | Dashboard → shared-cache 업로드 또는 Trial #0 다운로드 | 다른 Trial도 재사용 |
| 학습 데이터셋 | shared-cache → tmp-cache 복사 | 공유 + 빠른 로컬 I/O |
| 중간 결과 (해당 Trial만) | tmp-cache | Pod 종료 시 자동 정리 |
| epoch 체크포인트 | artifact-store (`upload_artifact()` 사용) | top_n_artifacts로 자동 관리 |

---

## 3. 빠른시작 (5분)

### 3-1. 토큰 발급
- AIAuto Web 접속: [https://dashboard.common.aiauto.pangyo.ainode.ai](https://dashboard.common.aiauto.pangyo.ainode.ai)
- "Generate New Token" 버튼 클릭
![](img/1_aiauto_dashboard_token.png)
- 생성된 Token 복사
![](img/2_aiauto_dashboard_token_copy.png)

### 3-2. 첫 최적화 실행
- 아래 예시 코드에 `<your-token-here>` 에 복사한 토큰 붙여넣기 (주의!! 두 군데 있음에 주의)
- Optuna 기본 개념 학습: 기존 Optuna 와 호환되므로 처음이라면 [Optuna 공식 튜토리얼](https://optuna.readthedocs.io/en/stable/tutorial/index.html)을 먼저 보길 권장
  - AIAutoController 를 사용하여 [](#studywrapper) 를 만들고 그 study_wrapper 에서 optimize() 호출한다는 점만 다름
```python
import aiauto
import optuna

# AIAuto 초기화
# 이 때 OptunaWorkspace 가 생성됨 (Front 또는 이전 실행으로 이미 존재하면 아래 설정값들은 무시됨)
ac = aiauto.AIAutoController(
    '<your-token-here>',
    # storage_size='500Mi',      # Journal Storage PVC 크기 (default: 500Mi, max: 10Gi)
    # artifact_store_size='2Gi', # Artifact Store PVC 크기 (default: 2Gi, max: 100Gi)
    # shared_cache_dir='/mnt/shared-cache', # Shared Cache 마운트 경로 (default: /mnt/shared-cache)
    # shared_cache_size='500Mi', # Shared Cache PVC 크기 (default: 500Mi, max: 4Gi)
)
# Study 생성
study_wrapper = ac.create_study(
    'my-first-study',  
    direction='minimize',  # can 'maximize'
    # sampler=optuna.samplers.TPESampler(),  # default, ref https://optuna.readthedocs.io/en/stable/reference/samplers/index.html
    # pruner=optuna.pruners.MedianPruner(),  # default, ref https://optuna.readthedocs.io/en/stable/reference/pruners.html
)

# Objective 함수 정의
def objective(trial):
    # objective 함수 안에서 import 하는 것 주의
    import aiauto
    import optuna
    from os.path import join
    
    # log 를 찍기 위한 TrialController 객체 생성 
    tc = aiauto.TrialController(trial)

    # 최적화 함수 작성
    x = trial.suggest_float('x', -10, 10)
    y = trial.suggest_float('y', -10, 10)
    value = (x - 2) ** 2 + (y - 3) ** 2
    
    # log
    tc.log(f'x={x:.2f}, y={y:.2f}, value={value:.4f}')
    
    # report
    trial.report(value, step=1)   # 중간 성능 보고
    
    # artifact 저장 시작 (필수는 아님 코드에서 제외해도 됨)
    # -------------------------------------------

    # 주의!!: artifact 저장을 위해 AIAutoController 를 objective 함수 안에서 다시 선언해야 함
    # (Front 또는 이전 실행으로 workspace가 이미 존재하면 아래 설정값들은 무시됨)
    ac_local = aiauto.AIAutoController(
        '<your-token-here>',
        # storage_size='500Mi',      # Journal Storage PVC 크기 (default: 500Mi, max: 10Gi)
        # artifact_store_size='2Gi', # Artifact Store PVC 크기 (default: 2Gi, max: 100Gi)
        # shared_cache_dir='/mnt/shared-cache', # Shared Cache 마운트 경로 (default: /mnt/shared-cache)
        # shared_cache_size='500Mi', # Shared Cache PVC 크기 (default: 500Mi, max: 4Gi)
    )
    try:
        # 파일명/내용에 trial 식별자 포함
        trial_id = f'{trial.study.study_name}_{trial.number}'
        filename = f'{trial_id}.txt'
        file_path = join(ac_local.get_artifact_tmp_dir(), filename)

        with open(file_path, 'w') as f:
          f.write(f'trial_id={trial_id}\n')
          f.write(f'final_score={value}\n')
          f.write(f'rand_value_x={x}\n')
          f.write(f'rand_value_y={y}\n')

        artifact_id = ac_local.upload_artifact(trial, file_path)
        tc.log(f'[artifact] saved {filename}, artifact_id={artifact_id}')
    except Exception as e:
        tc.log(f'[artifact] failed: {e}')
    finally:
        tc.flush()

    # -------------------------------------------
    # artifact 저장 종료 (필수는 아님 코드에서 제외해도 됨)

    return value

# 최적화 실행 (Kubernetes 클러스터에서 병렬 실행)
# 동시에 2개씩 총 10개 trial 생성
# 하나의 trial 당 1 cpu, 500Mi memory 의 pod 가 뜸
trialbatch_name = study_wrapper.optimize(
    objective,
    n_trials=10,  # default 로 전체 시도 trial 갯수
    parallelism=2,  # default 로 동시 실행 trial 갯수
    # use_gpu=False,  # default 로 gpu 미사용
    # gpu_model=None,  # default, 클러스터 상황에 맞춰 GPU 자동 선택 (use_gpu=True일 때만 의미 있음; gpu_model={'gpu_3090': 3, 'gpu_4090': 4} dict 합계 < n_trials 이면 나머지는 자동 선택); gpu_model='gpu_3090' 특정 gpu 만 선택도 가능;
    # runtime_image='ghcr.io/astral-sh/uv:python3.8-bookworm-slim',  # default(use_cpu) 로 기본 python uv
    # --- Image Pull Secret (둘 중 하나만 사용) ---
    # [방법1] registry 방식 (단일 registry, auth 자동 생성)
    # image_pull_registry='registry.gitlab.com',
    # image_pull_username='deploy-token-name',
    # image_pull_password='glpat-xxxxx',
    # [방법2] dockerconfigjson 방식 (여러 registry 지원)
    # image_pull_docker_config_json={
    #     "auths": {
    #         "ghcr.io": {"username": "user", "password": "token"},
    #         "registry.gitlab.com": {"username": "deploy-token", "password": "glpat-xxx"},
    #     }
    # },
    resources_requests={
        "cpu": "1",  # default(use_cpu): 1
        "memory": "500Mi",  # default(use_cpu): 1Gi
    },
    wait_option=aiauto.WaitOption.WAIT_ATLEAST_ONE_TRIAL, # default 로 최소 1개 trial 완료될 때까지 대기 (wait_option 기본값)
    # wait_timeout=600,  # default 로 wait_option 충족할 때 까지 기다는 max 시간
    # requirements_list=['numpy', 'torch',],  # or  # requirements_file='requirements.txt',
    # dev_shm_size="500Mi",  # /dev/shm 크기 (GPU 사용 시 자동 추가, default: 500Mi, max: 4Gi)
    # tmp_cache_dir="/mnt/tmp-cache",  # tmp-cache 마운트 경로 (default: /mnt/tmp-cache)
    # use_tmp_cache_mem=False,  # tmp-cache를 tmpfs로 사용 (default: False, disk-based emptyDir)
    # tmp_cache_size="500Mi",  # tmp-cache 크기 (default: 500Mi, max: 4Gi)
    # top_n_artifacts=5,  # 상위 N개 artifact만 보존 (default: 5, min: 1)
)

status = study_wrapper.get_status()
# 특정 TrialBatch 상태 확인
print(status)
```

### 3-3. Dashboard 결과 확인
- 최적화가 완료되면 Optuna Dashboard에서 Study 진행상황, 최적 파라미터, 시각화 그래프를 확인할 수 있습니다.
#### 3-3-1. [AIAuto Web OptunaWorkspace](https://dashboard.common.aiauto.pangyo.ainode.ai/workspace) tab 에 접속하여 `Open Dashboard` 링크 클릭
![](img/3_aiauto_dashboard_study.png)
#### 3-3-2. 하나의 Optuna Dashboard 에 여러 Study 출력
- Optuna Dashboard 는 Study 단위로 진행상황과 그래프를 표시합니다
- 아래 처럼 여러 study 가 하나의 Optuna Dashboard 에 보이며, 하나의 Study에 여러 TrialBatch가 포함 됨
![](img/4_optuna_dashboard_study.png)
#### 3-3-3. Study 안에서 그래프 확인
- 생성된 Study 안에서 그래프, Hyperparameter Importance, Optimization History 등 진행 상황과 그래프 확인 가능
![](img/5_optuna_dashboard_graph.png)
#### 3-3-4. 각 Trial 별 log Optuna Dashboard note 에서 확인
- 개별 trial 의 log 도 확인 가능
![](img/6_optuna_dashbaord_study_trialbatch.png)
#### 3-3-5. Pod 상태 확인 및 artifact 다운로드
- 개별 trial 의 artifact 는 [AIAuto Web TrialBatch](https://dashboard.common.aiauto.pangyo.ainode.ai/trialbatch) 의 `Pod 상태` 버튼을 눌러 확인 가능
![](img/7_aiauto_dashboard_trialbatch_artifact.png)
#### 3-3-6. Pruning 하는 경우 확인
- Optuna Dashboard 에서 그래프에서 빨간색으로 표시되는 것 확인
TODO
- 각 trial 의 상태에서 pruned 확인
TODO

### 3-4. 런타임 이미지

#### 3-4-1. 커스텀 이미지 사용 조건
`uv` 또는 `pip`가 실행 가능한 Python 3.8 이상 이미지이면 사용 가능합니다.

```python
study.optimize(
    objective,
    n_trials=10,
    runtime_image='your-registry.com/your-image:tag',  # 커스텀 이미지 지정
    # private registry인 경우 image_pull_secret 설정 필요 (아래 참고)
)
```

#### 3-4-2. 기본 제공 이미지 예시
**CPU (use_gpu=False, 기본값)**
- `ghcr.io/astral-sh/uv:python3.8-bookworm-slim` ~ `python3.13-bookworm-slim`

**GPU (use_gpu=True)**
- `pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime` 등 PyTorch 시리즈
- `tensorflow/tensorflow:2.13.0-gpu` 등 TensorFlow 시리즈

### 3-5. 리소스 설정
#### 3-5-1. CPU 사용
```python
study.optimize(
    objective,
    n_trials=10,  # default 로 전체 시도 trial 갯수
    parallelism=2,  # default 로 동시 실행 trial 갯수
    # use_gpu=False,  # default 로 gpu 미사용
    # gpu_model=None,  # default, 클러스터 상황에 맞춰 GPU 자동 선택 (use_gpu=True일 때만 의미 있음; gpu_model={'gpu_3090': 3, 'gpu_4090': 4} dict 합계 < n_trials 이면 나머지는 자동 선택); gpu_model='gpu_3090' 특정 gpu 만 선택도 가능;
    # runtime_image='ghcr.io/astral-sh/uv:python3.8-bookworm-slim',  # default(use_cpu) 로 기본 python uv
    # --- Image Pull Secret (둘 중 하나만 사용) ---
    # [방법1] registry 방식 (단일 registry, auth 자동 생성)
    # image_pull_registry='registry.gitlab.com',
    # image_pull_username='deploy-token-name',
    # image_pull_password='glpat-xxxxx',
    # [방법2] dockerconfigjson 방식 (여러 registry 지원)
    # image_pull_docker_config_json={
    #     "auths": {
    #         "ghcr.io": {"username": "user", "password": "token"},
    #         "registry.gitlab.com": {"username": "deploy-token", "password": "glpat-xxx"},
    #     }
    # },
    resources_requests={
        "cpu": "1",  # default(use_cpu): 1
        "memory": "500Mi",  # default(use_cpu): 1Gi
    },
    # wait_option=aiauto.WaitOption.WAIT_ATLEAST_ONE_TRIAL, # default 로 최소 1개 trial 완료될 때까지 대기 (wait_option 기본값)
    # wait_timeout=600,  # default 로 wait_option 충족할 때 까지 기다는 max 시간
    # requirements_list=['numpy', 'torch',],  # or  # requirements_file='requirements.txt',
    # dev_shm_size="500Mi",  # /dev/shm 크기 (GPU 사용 시 자동 추가, default: 500Mi, max: 4Gi)
    # tmp_cache_dir="/mnt/tmp-cache",  # tmp-cache 마운트 경로 (default: /mnt/tmp-cache)
    # use_tmp_cache_mem=False,  # tmp-cache를 tmpfs로 사용 (default: False, disk-based emptyDir)
    # tmp_cache_size="500Mi",  # tmp-cache 크기 (default: 500Mi, max: 4Gi)
    # top_n_artifacts=5,  # 상위 N개 artifact만 보존 (default: 5, min: 1)
)
```
#### 3-5-2. GPU 사용
```python
study.optimize(
    objective,
    n_trials=10,  # default 로 전체 시도 trial 갯수
    parallelism=2,  # default 로 동시 실행 trial 갯수
    use_gpu=True,  # default: False
    # gpu_model=None,  # default, 클러스터 상황에 맞춰 GPU 자동 선택 (use_gpu=True일 때만 의미 있음; gpu_model={'gpu_3090': 3, 'gpu_4090': 4} dict 합계 < n_trials 이면 나머지는 자동 선택); gpu_model='gpu_3090' 특정 gpu 만 선택도 가능;
    # runtime_image='pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime',  # default(use_gpu) 로 gpu 사용시 기본 python cuda
    # --- Image Pull Secret (둘 중 하나만 사용) ---
    # [방법1] registry 방식 (단일 registry, auth 자동 생성)
    # image_pull_registry='registry.gitlab.com',
    # image_pull_username='deploy-token-name',
    # image_pull_password='glpat-xxxxx',
    # [방법2] dockerconfigjson 방식 (여러 registry 지원)
    # image_pull_docker_config_json={
    #     "auths": {
    #         "ghcr.io": {"username": "user", "password": "token"},
    #         "registry.gitlab.com": {"username": "deploy-token", "password": "glpat-xxx"},
    #     }
    # },
    resources_requests={
        "cpu": "8",  # default(use_gpu): 2
        "memory": "16Gi",  # default(use_gpu): 4Gi
        "nvidia.com/gpu": "2",  # default(use_gpu): 1
    },
    # wait_option=aiauto.WaitOption.WAIT_ATLEAST_ONE_TRIAL, # default 로 최소 1개 trial 완료될 때까지 대기 (wait_option 기본값)
    # wait_timeout=600,  # default 로 wait_option 충족할 때 까지 기다는 max 시간
    requirements_list=['numpy', 'torch',],  # or  # requirements_file='requirements.txt',
    dev_shm_size="2Gi",  # /dev/shm 크기 (GPU 사용 시 자동 추가, default: 500Mi, max: 4Gi)
    # tmp_cache_dir="/mnt/tmp-cache",  # tmp-cache 마운트 경로 (default: /mnt/tmp-cache)
    # use_tmp_cache_mem=False,  # tmp-cache를 tmpfs로 사용 (default: False, disk-based emptyDir)
    # tmp_cache_size="500Mi",  # tmp-cache 크기 (default: 500Mi, max: 4Gi)
    # top_n_artifacts=5,  # 상위 N개 artifact만 보존 (default: 5, min: 1)
)
```

### 3-6. API 레퍼런스

#### 3-6-1. AIAutoController
- token 인증을 통해 원격으로 zerooneai kubenetes cluster 서버와 통신하기 위한 객체 
- 생성 시 OptunaWorkspace 를 초기화
- create_study() 로 optuna [Study](https://optuna.readthedocs.io/en/stable/reference/generated/optuna.study.Study.html) 의 wrapper 인 [StudyWrapper](#studywrapper) 를 생성
```python
import aiauto

# AIAuto 초기화 (Front 또는 이전 실행으로 workspace가 이미 존재하면 아래 설정값들은 무시됨)
ac = aiauto.AIAutoController(
    '<your-token>',
    # storage_size='500Mi',      # Journal Storage PVC 크기 (default: 500Mi, max: 10Gi)
    # artifact_store_size='2Gi', # Artifact Store PVC 크기 (default: 2Gi, max: 100Gi)
    # shared_cache_dir='/mnt/shared-cache', # Shared Cache 마운트 경로 (default: /mnt/shared-cache)
    # shared_cache_size='500Mi', # Shared Cache PVC 크기 (default: 500Mi, max: 4Gi)
)
study_wrapper = ac.create_study('study-name', direction='minimize')
```
##### 3-6-1-1. Parameters
- `token` (str): [Front](#1-토큰-발급)에서 발급받은 API 토큰
- `storage_size` (str, optional): Journal Storage PVC 크기 (default: '500Mi', max: 10Gi)
- `artifact_store_size` (str, optional): Artifact Store PVC 크기 (default: '2Gi', max: 100Gi)
- `shared_cache_dir` (str, optional): Shared Cache PVC 마운트 경로 - Trial Pod에서 접근하는 shared-cache 디렉토리 경로 (default: '/mnt/shared-cache')
- `shared_cache_size` (str, optional): Shared Cache PVC 크기 - 여러 trial pod에서 공유하는 캐시 스토리지, Front에서 업로드한 파일이 저장됨 (default: '500Mi', max: 4Gi)

> **주의**: `storage_size`, `artifact_store_size`, `shared_cache_dir`, `shared_cache_size` 매개변수는 **OptunaWorkspace가 처음 생성될 때만 적용**됩니다. Front에서 workspace를 먼저 생성했거나 이전 코드 실행으로 이미 존재하는 경우, 이 값들은 무시되고 기존 workspace 설정이 유지됩니다. workspace 설정을 변경하려면 [Front Workspace 페이지](https://dashboard.common.aiauto.pangyo.ainode.ai/workspace)에서 기존 workspace를 삭제 후 다시 생성해야 합니다.

##### 3-6-1-2. Methods
- `create_study(study_name, direction, ...)`: optuna [Study](https://optuna.readthedocs.io/en/stable/reference/generated/optuna.study.Study.html) 의 wrapper 인 [StudyWrapper](#studywrapper) 를 생성
  - `study_name` (str): Study 이름 (unique 해야 함, DNS-1123 subdomain 규칙: 소문자/숫자/하이픈만 허용, 최대 63자)
  - `direction` (str): Single-objective 인 경우 문자열 사용 "minimize" 또는 "maximize" (default: minimize), direction 과 directions 둘 중 하나만 지정해야 한다
  - `directions` (List[str]): Multi-objective 인 경우 문자열의 리스트 사용, direction 과 directions 둘 중 하나만 지정해야 한다
  - `sampler` (optuna.samplers.BaseSampler): Optuna Sampler 와 호환 (default: TPESampler), ref https://optuna.readthedocs.io/en/stable/reference/samplers/index.html
  - `pruner` (optuna.pruners.BasePruner): Optuna Pruner 와 호한 (default: MedianPruner), ref https://optuna.readthedocs.io/en/stable/reference/pruners.html
- `get_storage` (): Optuna study/trial 정보를 저장할 Storage 객체 반환 (OptunaWorkspace 와 연동되어야 하기 때문에 꼭 이 객체를 사용해야한다) [6-checkpoint-저장-artifact](#6-checkpoint-저장-artifact) 참고
- `get_artifact_store` (): Artifact Store 객체 반환
- `get_artifact_tmp_dir` (): Artifact 임시 디렉토리 경로 (이 경로에 파일을 저장한 후 `upload_artifact` 호출)
- `upload_artifact` (trial, file_path): Artifact 업로드 및 `artifact_id` user_attr 자동 설정
  - `trial` (optuna.trial.Trial): artifact를 연결할 trial
  - `file_path` (str): 업로드할 파일 경로
  - 반환값: artifact_id (str)

#### 3-6-2. StudyWrapper
- [Study](https://optuna.readthedocs.io/en/stable/reference/generated/optuna.study.Study.html) 는 기존 Optuna 에서 사용하는 객체, StudyWrapper 는 이를 Wrapping 하여 aiauto 에서 사용하는 객체, get_study() 통하여 wrapper 에서 부터 진짜 객체를 가져올 수 있다
```python
study_wrapper.optimize(
    objective,
    n_trials=10,  # default 로 전체 시도 trial 갯수
    parallelism=2,  # default 로 동시 실행 trial 갯수
    # use_gpu=False,  # default 로 gpu 미사용
    # gpu_model=None,  # default, 클러스터 상황에 맞춰 GPU 자동 선택 (use_gpu=True일 때만 의미 있음; gpu_model={'gpu_3090': 3, 'gpu_4090': 4} dict 합계 < n_trials 이면 나머지는 자동 선택); gpu_model='gpu_3090' 특정 gpu 만 선택도 가능;
    # runtime_image='ghcr.io/astral-sh/uv:python3.8-bookworm-slim',  # default(use_cpu) 로 기본 python uv
    # --- Image Pull Secret (둘 중 하나만 사용) ---
    # [방법1] registry 방식 (단일 registry, auth 자동 생성)
    # image_pull_registry='registry.gitlab.com',
    # image_pull_username='deploy-token-name',
    # image_pull_password='glpat-xxxxx',
    # [방법2] dockerconfigjson 방식 (여러 registry 지원)
    # image_pull_docker_config_json={
    #     "auths": {
    #         "ghcr.io": {"username": "user", "password": "token"},
    #         "registry.gitlab.com": {"username": "deploy-token", "password": "glpat-xxx"},
    #     }
    # },
    resources_requests={
        "cpu": "1",  # default(use_cpu): 1
        "memory": "500Mi",  # default(use_cpu): 1Gi
    },
    # wait_option=aiauto.WaitOption.WAIT_ATLEAST_ONE_TRIAL, # default 로 최소 1개 trial 완료될 때까지 대기 (wait_option 기본값)
    # wait_timeout=600,  # default 로 wait_option 충족할 때 까지 기다는 max 시간
    # requirements_list=['numpy', 'torch',],  # or  # requirements_file='requirements.txt',
    # dev_shm_size="500Mi",  # /dev/shm 크기 (GPU 사용 시 자동 추가, default: 500Mi, max: 4Gi)
    # tmp_cache_dir="/mnt/tmp-cache",  # tmp-cache 마운트 경로 (default: /mnt/tmp-cache)
    # use_tmp_cache_mem=False,  # tmp-cache를 tmpfs로 사용 (default: False, disk-based emptyDir)
    # tmp_cache_size="500Mi",  # tmp-cache 크기 (default: 500Mi, max: 4Gi)
    # top_n_artifacts=5,  # 상위 N개 artifact만 보존 (default: 5, min: 1)
)
status = study_wrapper.get_status()  # 진행 상황 확인
print(status)
```
##### 3-6-2-1. Methods
- `get_study` (): study_wrapper 가 아닌, optuna 의 [Study](https://optuna.readthedocs.io/en/stable/reference/generated/optuna.study.Study.html) 를 반환
- `optimize` (): zerooneai 의 kubernetes cluster 에서 병렬 실행되는 최적화를 실행 → **str (trialbatch_name) 반환**
  - `objective` (Callable[oputna.trial, None]): optuna 에서 일반적으로 구현해서 사용하는 Objective 함수를 이 매개변수로 전달
  - `n_trials` (int): 실행할 전체 trial 개수 (default: 10) (failed or pruned 갯수 포함)
  - `parallelism` (int): 동시 실행 Pod 개수 (default: 2)
  - `use_gpu` (bool): GPU 사용 여부 (default: False)
  - `gpu_model` (Optional[Union[str, Dict[str, int]]]): GPU 모델 지정 (default: None)
    - `"gpu_3090"` / `"gpu_4090"` 문자열로 특정 gpu 만 사용 또는 `{ "gpu_3090": 3, "gpu_4090": 4 }` 형태, 해당 gpu 로 돌릴 trial 갯수
    - dict 합계가 `n_trials`보다 작으면 **나머지는 클러스터 상황에 맞춰 GPU 자동 선택**
      - 예: `n_trials=10`, `gpu_model={'gpu_3090': 3, 'gpu_4090': 4}` → 7개는 고정, 나머지 3개는 자동 선택
    - dict 합계가 `n_trials`를 초과하면 오류
    - 미지정(None) 시 클러스터 상황에 맞춰 GPU 자동 선택
  - `runtime_image` (str): Docker 이미지 (default: "ghcr.io/astral-sh/uv:python3.8-bookworm-slim" / "pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime"(use_gpu))
    - 커스텀 이미지 사용 시 **Python과 pip 또는 uv 필수** (runner가 의존성 설치에 사용)
  - **Image Pull Secret** (아래 두 방식 중 하나만 사용):
    - **[방법1] registry 방식** (단일 registry, auth 자동 생성):
      - `image_pull_registry` (str): Private registry 주소 (예: "registry.gitlab.com", "ghcr.io")
      - `image_pull_username` (str): Registry 사용자명 또는 deploy token 이름
      - `image_pull_password` (str): Registry 비밀번호 또는 access token
    - **[방법2] dockerconfigjson 방식** (여러 registry 지원):
      - `image_pull_docker_config_json` (dict): Docker config JSON 형식
      - 예: `{"auths": {"ghcr.io": {"username": "user", "password": "token"}, "registry.gitlab.com": {...}}}`
  - `requirements_list` (List[str]): 추가 설치할 패키지 리스트 (default: list 가 비어있어도 optuna, grpcio, protobuf, aiauto-client, optuna-dashboard 는 자동 설치)
  - `resources_requests` (dict): Pod 리소스 요청
    - `"cpu"`: (str) CPU 코어 수 (default: "1" / "2"(use_gpu))
    - `"memory"`: (str) 메모리 용량 (default: "1Gi" / "4Gi"(use_gpu))
    - `"nvidia.com/gpu"`: (str) GPU 개수 (default: "1"(use_gpu)) (max: 4)
  - `resources_limits` (dict): Pod 리소스 제한 (default: 빈 dict, requests 기반으로 자동 설정)
    - `"cpu"`: (str) CPU 코어 수 제한
    - `"memory"`: (str) 메모리 용량 제한
    - `"nvidia.com/gpu"`: (str) GPU 개수 제한
  - `wait_option` (WaitOption): Trial 완료 대기 옵션 (default: WaitOption.WAIT_ATLEAST_ONE_TRIAL)
    - `WaitOption.WAIT_NO`: 대기하지 않고 즉시 반환
    - `WaitOption.WAIT_ATLEAST_ONE_TRIAL`: 최소 1개 trial 완료 시 반환 (기본값)
      - 완료된 trial이 FAILED/PRUNED 일 수 있음, best_trial 접근 시 주의
    - `WaitOption.WAIT_ALL_TRIALS`: 모든 trial 완료 시 반환
  - `wait_timeout` (int): 대기 최대 시간(초) (default: 600)
  - `dev_shm_size` (str): /dev/shm emptyDir 크기 (default: "500Mi", GPU 사용 시만 적용, max: 4Gi)
  - `tmp_cache_dir` (str): tmp-cache emptyDir 마운트 경로 (default: "/mnt/tmp-cache")
  - `use_tmp_cache_mem` (bool): tmp-cache를 tmpfs(Memory medium)로 사용 여부 (default: False, disk-based emptyDir)
  - `tmp_cache_size` (str): tmp-cache emptyDir 크기 (default: "500Mi", max: 4Gi)
  - `top_n_artifacts` (int): 보존할 상위 artifact 개수 (default: 5, min: 1)
    - Trial 완료 시 objective value 기준 상위 N개 artifact만 보존하고 나머지는 삭제
    - 예: `top_n_artifacts=3` → 최적화 방향(minimize/maximize)에 따라 상위 3개 trial의 artifact만 보존
- `get_status` (trialbatch_name: Optional[str] = None, include_trials: bool = False): Study 또는 특정 TrialBatch의 상태 정보 조회
  - `trialbatch_name`
    - 미지정: Study의 모든 TrialBatch를 trialbatches dict로 반환
    - 지정: 해당 TrialBatch만 trialbatches dict에 담아 반환
  - `include_trials`: True일 때 completed_trials 상세 정보 포함 (default: False)
  - 반환 구조: `{"study_name": str, "trialbatches": {tb_name: {"count_active": int, ...}}, "dashboard_url": str, "updated_at": str}`
- `is_trial_finished` (trial_identifier: Union[int, str], trialbatch_name: Optional[str] = None): 특정 trial이 완료되었는지 확인 → **bool 반환**
  - `trial_identifier`: trial 번호(int) 또는 pod 이름(str)
  - `trialbatch_name`: 확인할 TrialBatch 이름. **trial 번호 사용 시 필수** (None이면 가장 최근 TrialBatch 사용)
  - 주의: trial 번호는 TrialBatch 내에서만 유니크하므로 trialbatch_name 명시 필요
- `wait` (trial_identifier: Union[int, str], trialbatch_name: Optional[str] = None, timeout: int = 600): 특정 trial이 완료될 때까지 대기 → **bool 반환**
  - `trial_identifier`: trial 번호(int) 또는 pod 이름(str)
  - `trialbatch_name`: 확인할 TrialBatch 이름. **trial 번호 사용 시 필수** (None이면 가장 최근 TrialBatch 사용)
  - `timeout`: 최대 대기 시간(초)
  - 반환값: 완료 시 True, 타임아웃 시 False
  - 주의: trial 번호는 TrialBatch 내에서만 유니크하므로 trialbatch_name 명시 필요

#### 3-6-3. TrialController
- Trial 실행 중 Optuna 의 save_note 기능을 활용하여 note (AIAuto Dashboard 에서 확인 가능) 에 로그를 출력하게 도와주는 객체
```python
import aiauto

def objective(trial):
    tc = aiauto.TrialController(trial)
    tc.log('Training started')
    # ... training code ...
    tc.flush()  # 로그 즉시 저장
```
##### 3-6-3-1. Parameters
- `trial` (optuna.trial.Trial, optuna.trial.FrozenTrial): objective 함수의 매개변수로 넘어오는 trial 을 Wrapping 해서 trialController 를 만든다
  - ask/tell 인 경우도 호환 되어야 하기 때문에 FrozenTrial 도 지원
##### 3-6-3-2. Methods
- `get_trial` (): TrialController 객체 생성 시 매개변수로 넣었던 trial 을 꺼낸다
- `log` (str): Optuna 의 save_note 에 로그를 기록, 성능상 이슈로 버퍼에 저장해두고 5개로그마다 저장한다
  - 로그 저장 주기
    - 5개 로그마다 자동 저장
    - `tc.flush()` 호출 시 즉시 저장
- `flush` (): 5개 로그 조건을 무시하고 지금 당장 로그를 저장한다. Optuna Callback 기능으로 종료 시 자동으로 호출 됨
- `set_user_attr` (key: str, value: Any): Trial에 사용자 정의 속성을 저장
  - 예약된 key(`pod_name`, `trialbatch_name`, `gpu_name`, `artifact_id`, `artifact_removed`)는 시스템에서 사용하므로 설정 불가
  - `artifact_id`는 `ac.upload_artifact()` 호출 시 자동 설정됨

---

## 4. 중요 주의사항

### 4-1. Objective 함수 작성 규칙 ⚠️
- 모든 import 는 objective 함수 내부에 작성해야 한다
- objective 함수는 사용자의 local 에서 실행되는게 아니라
  - serialize 되어서 원격에 있는 zerooneai 의 kubenetes cluster 에서 동작하므로 
  - objective 함수 안에서 import 해줘야 같이 serialize 되어 정상 동작한다
#### 4-1-1. 잘못된 import 예시 ❌
```python
import torch

def objective(trial):
    model = torch.nn.Linear(10, 1)  # 실패: torch를 찾을 수 없음
```
#### 4-1-2. 올바른 import 예시 ✅
```python
def objective(trial):
    import torch  # 함수 내부에서 import
    model = torch.nn.Linear(10, 1)
```

### 4-2. AIAutoController objective 함수 안 재정의 ⚠️
- Artifact 를 저장하기 위해 objective 함수 안에서 `AIAutoController.upload_artifact()` 를 사용하려면
- objective 함수 내부에서 `AIAutoController`를 재선언 해야한다
```python
def objective(trial):
    import aiauto
    import optuna

    # Objective 내부에서 재선언 (singleton이라 문제없음, Front 또는 이전 실행으로 workspace가 이미 존재하면 아래 설정값들은 무시됨)
    ac = aiauto.AIAutoController(
        '<your-token>',
        # storage_size='500Mi',      # default: 500Mi, max: 10Gi
        # artifact_store_size='2Gi', # default: 2Gi, max: 100Gi
    )

    # Artifact 저장
    artifact_id = ac.upload_artifact(trial, 'model.pth')
```

### 4-3. Jupyter Notebook 사용 시 주의사항 ⚠️
- Python REPL에서 정의한 objective 함수는 optimize 에 사용할 수 없다
  - Python REPL에서 정의한 함수는 serialize 할 수 없다
  - `%%writefile` 매직 커맨드로 파일에 저장한 후 import 해야한다
- 함수 정의 위에 `%%writefile` 매직 커맨드 사용
```python
%%writefile my_objective.py
def objective(trial):
    import aiauto
    tc = aiauto.TrialController(trial)
    x = trial.suggest_float('x', -10, 10)
    return x ** 2
```
- Jupyter Notebook directory 안에 파일이 생성된 걸 확인할 수 있고
- 다음 cell 에서 import 하여 사용할 수 있다
```python
from my_objective import objective

study.optimize(
    objective, 
    n_trials=10,  # default 로 전체 시도 trial 갯수
    parallelism=2,  # default 로 동시 실행 trial 갯수
    # use_gpu=False,  # default 로 gpu 미사용
    # gpu_model=None,  # default, 클러스터 상황에 맞춰 GPU 자동 선택 (use_gpu=True일 때만 의미 있음; gpu_model={'gpu_3090': 3, 'gpu_4090': 4} dict 합계 < n_trials 이면 나머지는 자동 선택); gpu_model='gpu_3090' 특정 gpu 만 선택도 가능;
    # runtime_image='ghcr.io/astral-sh/uv:python3.8-bookworm-slim',  # default(use_cpu) 로 기본 python uv
    # --- Image Pull Secret (둘 중 하나만 사용) ---
    # [방법1] registry 방식 (단일 registry, auth 자동 생성)
    # image_pull_registry='registry.gitlab.com',
    # image_pull_username='deploy-token-name',
    # image_pull_password='glpat-xxxxx',
    # [방법2] dockerconfigjson 방식 (여러 registry 지원)
    # image_pull_docker_config_json={
    #     "auths": {
    #         "ghcr.io": {"username": "user", "password": "token"},
    #         "registry.gitlab.com": {"username": "deploy-token", "password": "glpat-xxx"},
    #     }
    # },
    # resources_requests={
    #     "cpu": "1",  # default(use_cpu): 1
    #     "memory": "1Gi",  # default(use_cpu): 1Gi
    # },
    # wait_option=aiauto.WaitOption.WAIT_ATLEAST_ONE_TRIAL, # default 로 최소 1개 trial 완료될 때까지 대기 (wait_option 기본값)
    # wait_timeout=600,  # default 로 wait_option 충족할 때 까지 기다는 max 시간
    # requirements_list=['numpy', 'torch',],  # or  # requirements_file='requirements.txt',
    # dev_shm_size="500Mi",  # /dev/shm 크기 (GPU 사용 시 자동 추가, default: 500Mi, max: 4Gi)
    # tmp_cache_dir="/mnt/tmp-cache",  # tmp-cache 마운트 경로 (default: /mnt/tmp-cache)
    # use_tmp_cache_mem=False,  # tmp-cache를 tmpfs로 사용 (default: False, disk-based emptyDir)
    # tmp_cache_size="500Mi",  # tmp-cache 크기 (default: 500Mi, max: 4Gi)
    # top_n_artifacts=5,  # 상위 N개 artifact만 보존 (default: 5, min: 1)
)
```

### 4-4. 예약된 user_attr 키 ⚠️
시스템이 내부적으로 사용하는 user_attr 키는 사용자가 설정할 수 없습니다.

**예약된 키 목록:**
- `pod_name`: Pod <-> Trial 매칭용
- `trialbatch_name`: TrialBatch 식별자
- `gpu_name`: 실제 할당된 GPU 모델 (nvidia-smi 조회 결과)
- `artifact_id`: Artifact 다운로드 링크
- `artifact_removed`: Artifact 삭제 플래그

#### 4-4-1. 자동 검증 (optimize() 사용 시) ✅
`optimize()` 호출로 실행되는 trial에서 `TrialController.set_user_attr()`를 사용하면 예약 키 검증이 자동으로 수행됩니다.

```python
def objective(trial):
    import aiauto
    tc = aiauto.TrialController(trial)

    # 정상 동작: 커스텀 키 설정
    tc.set_user_attr('my_metric', 0.95)
    tc.set_user_attr('custom_data', {'key': 'value'})

    # 오류 발생: 예약된 키 설정 시도
    tc.set_user_attr('pod_name', 'custom-pod')  # ValueError 발생

    return trial.suggest_float('x', 0, 1)

study.optimize(objective, n_trials=10)
```

#### 4-4-2. Ask/Tell 패턴 주의사항 ⚠️
Ask/Tell 패턴으로 `study.ask()`를 통해 trial을 직접 받아온 경우, `trial.set_user_attr()`를 직접 호출하면 **검증이 우회**됩니다.

**시스템이 나중에 예약된 키를 덮어쓸 수 있으므로, Ask/Tell 패턴에서는 user_attr 설정을 아예 하지 마세요.**

```python
# Ask/Tell 패턴 사용 시 (검증 우회됨)
study = controller.create_study('my-study', direction='minimize')
real_study = study.get_study()

trial = real_study.ask()  # 직접 trial 객체 받기
trial.set_user_