{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "37f1fed1-191f-4ec9-86d0-0867c190c66e",
   "metadata": {},
   "source": [
    "# Kanana 모델 파인튜닝 및 서빙 API 생성 파이프라인\n",
    "\n",
    "이 노트북은 Kanana LLM 모델을 파인튜닝하고 KServe를 통해 서빙 API를 생성하는 전체 파이프라인을 구성합니다.\n",
    "\n",
    "## 파이프라인 구성\n",
    "1. **데이터 수집**: 학습 데이터를 Object Storage에서 다운로드\n",
    "2. **모델 파인튜닝**: LoRA 기반 파인튜닝 수행\n",
    "3. **모델 서빙**: KServe InferenceService를 통한 vLLM 배포"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 121,
   "id": "a6bcd44f-c844-4236-ba19-bcea60ea2154",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 필요한 라이브러리 import\n",
    "import os\n",
    "import uuid\n",
    "from kakaocloud_kbm import KbmPipelineClient\n",
    "from kfp import kubernetes\n",
    "import kfp.dsl as dsl"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 122,
   "id": "5580602d-ae31-48e2-8448-807df635673c",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/opt/conda/lib/python3.11/site-packages/kakaocloud_kbm/__init__.py:184: FutureWarning: This client only works with Kubeflow Pipeline v2.0.0-beta.2 and later versions.\n",
      "  warnings.warn(\n"
     ]
    }
   ],
   "source": [
    "# KBM Kubeflow Pipeline 클라이언트 초기화\n",
    "# Kubeflow Pipeline 서버에 연결하기 위한 인증 정보 설정\n",
    "os.environ[\"KUBEFLOW_HOST\"] = \"https://nipagpu.kakaocloud.com\"\n",
    "os.environ[\"KUBEFLOW_USERNAME\"] = \"clare.roh@kakaoenterprise.com\"\n",
    "os.environ[\"KUBEFLOW_PASSWORD\"] = \"clare123!@\"\n",
    "\n",
    "# Pipeline 클라이언트 생성\n",
    "# verify_ssl=False 옵션은 TLS 인증서 검증이 필요한 경우 사용\n",
    "client = KbmPipelineClient()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 187,
   "id": "d6eb91d4-ac3a-4149-8e5a-c0f2f2c2552d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model Name: kanana-model-4471d88e\n",
      "KServe InferenceService Name: kanana-isvc-4471d88e\n",
      "Model PVC Name: kanana-ft-pvc-4471d88e\n"
     ]
    }
   ],
   "source": [
    "# 파이프라인 변수 설정\n",
    "# 현재 노트북이 실행되는 Kubernetes 네임스페이스 추출\n",
    "KBM_NAMESPACE = os.environ['NB_PREFIX'].split('/')[2]\n",
    "\n",
    "# 컴포넌트 파일 저장 경로 설정\n",
    "COMPONENT_PATH = 'components'\n",
    "SERVE_ENPOINT_PATH = os.path.join(COMPONENT_PATH, 'kanana_finetune')\n",
    "\n",
    "# 고유 작업 ID 생성 (각 파이프라인 실행을 구분하기 위함)\n",
    "TASK_UUID = uuid.uuid1().hex[:8]\n",
    "\n",
    "# 리소스 이름 생성\n",
    "PVC_NAME = f\"kanana-ft-pvc-{TASK_UUID}\"  # PersistentVolumeClaim 이름\n",
    "MODEL_NAME = f\"kanana-model-{TASK_UUID}\"  # 모델 이름\n",
    "KSERVE_ISVC_NAME = f\"kanana-isvc-{TASK_UUID}\"  # 모델 서빙 API 이름\n",
    "EPOCH_NUM = 10  # 학습 에포크 수\n",
    "\n",
    "print(f\"Model Name: {MODEL_NAME}\")\n",
    "print(f\"KServe InferenceService Name: {KSERVE_ISVC_NAME}\")\n",
    "print(f\"Model PVC Name: {PVC_NAME}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "40d3adbc-bb9f-4210-9850-44fb5223cdc9",
   "metadata": {},
   "source": [
    "## 파이프라인 컴포넌트 정의\n",
    "\n",
    "파이프라인은 세 가지 주요 컴포넌트로 구성됩니다:\n",
    "1. **데이터 수집 컴포넌트**: 학습 데이터 다운로드\n",
    "2. **모델 파인튜닝 컴포넌트**: LoRA 기반 파인튜닝\n",
    "3. **모델 서빙 컴포넌트**: KServe InferenceService 배포"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 188,
   "id": "0d171419-e1a8-44d3-879c-2cf1dee51c8e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Component directory created: components/kanana_finetune\n"
     ]
    }
   ],
   "source": [
    "%%bash -s \"{SERVE_ENPOINT_PATH}\"\n",
    "\n",
    "mkdir -p ${1}\n",
    "echo \"Component directory created: ${1}\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c7c0f52f-f1bb-4c5f-bf06-7c7c93456691",
   "metadata": {},
   "source": [
    "### 1. 데이터 수집 컴포넌트\n",
    "\n",
    "Object Storage에서 학습 데이터를 다운로드하는 컴포넌트입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 189,
   "id": "6c7ce4cd-5f58-445a-9288-b3028d8154a2",
   "metadata": {},
   "outputs": [],
   "source": [
    "@dsl.component(\n",
    "    packages_to_install=['requests'],\n",
    "    base_image='python:3.11'\n",
    ")\n",
    "def download_dataset(kc_kbm_os_train_url: str):\n",
    "    \"\"\"\n",
    "    Object Storage에서 학습 데이터를 다운로드하는 컴포넌트\n",
    "    \n",
    "    Args:\n",
    "        kc_kbm_os_train_url: 학습 데이터 CSV 파일의 Object Storage URL\n",
    "    \"\"\"\n",
    "    import os\n",
    "    from requests import get\n",
    "\n",
    "    def download(url, dist_dir, file_name=None):\n",
    "        \"\"\"URL에서 파일을 다운로드하여 지정된 디렉토리에 저장\"\"\"\n",
    "        if not file_name:\n",
    "            file_name = url.split('/')[-1]\n",
    "        \n",
    "        file_path = os.path.join(dist_dir, file_name)\n",
    "        with open(file_path, \"wb\") as file:\n",
    "            response = get(url)\n",
    "            response.raise_for_status()  # HTTP 에러 체크\n",
    "            file.write(response.content)\n",
    "        print(f\"Downloaded: {file_name} to {dist_dir}\")\n",
    "    \n",
    "    # PVC 마운트 경로 (파이프라인에서 /data로 마운트됨)\n",
    "    pvc_data_path = \"/data\"\n",
    "\n",
    "    # 기본 URL이 제공되지 않은 경우 샘플 데이터 URL 사용\n",
    "    if not kc_kbm_os_train_url:\n",
    "        kc_kbm_os_train_url = 'https://objectstorage.kr-central-2.kakaocloud.com/v1/c11fcba415bd4314b595db954e4d4422/public/tutorial/kubeflow/kubeflow-tensorboard/data/sample_train_data.csv'\n",
    "    \n",
    "    # 학습 데이터 다운로드\n",
    "    download(kc_kbm_os_train_url, pvc_data_path, \"sample_train_data.csv\")\n",
    "\n",
    "    # 다운로드된 파일 목록 확인\n",
    "    print(f\"Downloaded files in {pvc_data_path}:\")\n",
    "    print(os.listdir(pvc_data_path))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6ded13db-e7de-4f69-a873-29cd6b95c00a",
   "metadata": {},
   "source": [
    "### 2. Kanana 모델 파인튜닝 컴포넌트\n",
    "\n",
    "Kubeflow Trainer를 사용하여 Kanana 모델을 LoRA 기반으로 파인튜닝하는 컴포넌트입니다.\n",
    "- PEFT (Parameter-Efficient Fine-Tuning) 라이브러리의 LoRA 기법 사용\n",
    "- Alpaca 형식의 프롬프트 템플릿 적용\n",
    "- GPU 메모리 효율적인 학습 수행"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8f9059ff-8df9-4456-a1a2-266dd2bbca75",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/tmp/ipykernel_525/2247505317.py:1: DeprecationWarning: output_component_file parameter is deprecated and will eventually be removed. Please use `Compiler().compile()` to compile a component instead.\n",
      "  @dsl.component(\n"
     ]
    }
   ],
   "source": [
    "@dsl.component(\n",
    "    packages_to_install=['kubeflow'],\n",
    "    install_kfp_package=True,\n",
    "    base_image='python:3.11',\n",
    "    output_component_file=f'{SERVE_ENPOINT_PATH}/train_component.yaml'\n",
    ")\n",
    "def finetune_kanana_model(\n",
    "    train_job_id: dsl.Output[dsl.Artifact],\n",
    "    epoch_num: str,\n",
    "    namespace: str,\n",
    "    pvc_name: str,\n",
    "    job_name: str,\n",
    "):\n",
    "    \"\"\"\n",
    "    Kanana 모델을 LoRA 기반으로 파인튜닝하는 컴포넌트\n",
    "    \n",
    "    Args:\n",
    "        epoch_num: 학습 에포크 수\n",
    "        namespace: Kubernetes 네임스페이스\n",
    "        pvc_name: 데이터 저장용 PVC 이름\n",
    "        job_name: TrainJob 이름\n",
    "    \"\"\"\n",
    "    from kubeflow.trainer import TrainerClient, CustomTrainer\n",
    "    import os\n",
    "    import time\n",
    "\n",
    "    def finetune_kanana(model_name: str, epoch_num: str, pvc_data_path: str):\n",
    "        \"\"\"\n",
    "        TrainJob Pod에서 실행될 학습 함수\n",
    "        LoRA 기반 파인튜닝을 수행합니다.\n",
    "        \"\"\"\n",
    "        from transformers import (\n",
    "            AutoModelForCausalLM,\n",
    "            AutoTokenizer,\n",
    "            TrainingArguments,\n",
    "            Trainer,\n",
    "            TrainerCallback,\n",
    "        )\n",
    "        from peft import LoraConfig, get_peft_model\n",
    "        from datasets import Dataset\n",
    "        import torch\n",
    "        import os\n",
    "        \n",
    "        # 작업 디렉토리 설정\n",
    "        os.chdir(\"/\")\n",
    "        \n",
    "        # GPU 환경 설정\n",
    "        os.environ[\"NVIDIA_VISIBLE_DEVICES\"] = \"0\"\n",
    "        os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"0\"\n",
    "        \n",
    "        print(f\"PyTorch version: {torch.__version__}\")\n",
    "        print(f\"CUDA available: {torch.cuda.is_available()}\")\n",
    "        \n",
    "        # 1. 모델 및 토크나이저 로드\n",
    "        print(\"=\" * 80)\n",
    "        print(\"Step 1: Loading LLM Model and Tokenizer\")\n",
    "        print(\"=\" * 80)\n",
    "        \n",
    "        tokenizer = AutoTokenizer.from_pretrained(model_name, padding_side=\"left\")\n",
    "        # Llama 타입 모델은 pad_token을 eos_token으로 설정 필요\n",
    "        tokenizer.pad_token = tokenizer.eos_token\n",
    "        \n",
    "        # 기본 모델 로드 (bfloat16으로 메모리 효율성 향상)\n",
    "        base_model = AutoModelForCausalLM.from_pretrained(\n",
    "            model_name,\n",
    "            torch_dtype=torch.bfloat16,\n",
    "            trust_remote_code=True,\n",
    "            device_map=\"auto\"\n",
    "        )\n",
    "        \n",
    "        # 2. LoRA 설정 및 적용\n",
    "        print(\"=\" * 80)\n",
    "        print(\"Step 2: Setting up LoRA Configuration\")\n",
    "        print(\"=\" * 80)\n",
    "        \n",
    "        lora_config = LoraConfig(\n",
    "            r=8,  # LoRA rank (낮을수록 파라미터 수 감소)\n",
    "            lora_alpha=32,  # LoRA alpha (스케일링 팩터)\n",
    "            lora_dropout=0.1,  # Dropout 비율\n",
    "            target_modules=[\"q_proj\", \"k_proj\", \"v_proj\"],  # 적용할 모듈\n",
    "            task_type=\"CAUSAL_LM\",\n",
    "        )\n",
    "        \n",
    "        model = get_peft_model(base_model, lora_config)\n",
    "        model.print_trainable_parameters()  # 학습 가능한 파라미터 수 출력\n",
    "        \n",
    "        # 3. 데이터셋 로드 및 전처리\n",
    "        print(\"=\" * 80)\n",
    "        print(\"Step 3: Loading and Processing Dataset\")\n",
    "        print(\"=\" * 80)\n",
    "        \n",
    "        train_data_path = f\"{pvc_data_path}/sample_train_data.csv\"\n",
    "        dataset = Dataset.from_csv(train_data_path)\n",
    "\n",
    "        # Alpaca 형식 프롬프트 템플릿 적용\n",
    "        def formatting_prompts_func(examples):\n",
    "            \"\"\"Alpaca 형식으로 프롬프트 포맷팅\"\"\"\n",
    "            alpaca_prompt = \"\"\"Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n",
    "\n",
    "### Instruction:\n",
    "{}\n",
    "\n",
    "### Input:\n",
    "{}\n",
    "\n",
    "### Response:\n",
    "{}\"\"\"\n",
    "\n",
    "            instructions = examples[\"instruction\"]\n",
    "            inputs = examples[\"input\"]\n",
    "            outputs = examples[\"output\"]\n",
    "            eos_token = tokenizer.eos_token\n",
    "\n",
    "            texts = []\n",
    "            for instruction, input_text, output in zip(instructions, inputs, outputs):\n",
    "                # EOS 토큰 추가 필수 (없으면 생성이 무한 반복될 수 있음)\n",
    "                text = alpaca_prompt.format(instruction, input_text, output) + eos_token\n",
    "                texts.append(text)\n",
    "\n",
    "            return {\"text\": texts}\n",
    "\n",
    "        # 프롬프트 포맷팅 적용\n",
    "        dataset = dataset.map(formatting_prompts_func, batched=True)\n",
    "        # 불필요한 컬럼 제거 (CSV 인덱스 컬럼 등)\n",
    "        if 'Unnamed: 0' in dataset.column_names:\n",
    "            dataset = dataset.remove_columns(['Unnamed: 0'])\n",
    "        \n",
    "        # 토크나이징\n",
    "        def tokenize_function(examples):\n",
    "            \"\"\"텍스트를 토큰으로 변환\"\"\"\n",
    "            tokens = tokenizer(examples[\"text\"], padding=True, return_tensors=\"pt\")\n",
    "            tokens[\"labels\"] = tokens[\"input_ids\"]  # Language modeling을 위한 labels\n",
    "            return tokens\n",
    "\n",
    "        dataset = dataset.map(tokenize_function, batched=True, remove_columns=[\"text\"])\n",
    "        print(\"Dataset processing complete\")\n",
    "\n",
    "        # 4. 학습 설정 및 Trainer 초기화\n",
    "        print(\"=\" * 80)\n",
    "        print(\"Step 4: Setting up Trainer\")\n",
    "        print(\"=\" * 80)\n",
    "        \n",
    "        class TrainingCallback(TrainerCallback):\n",
    "            \"\"\"학습 진행 상황 로깅용 콜백\"\"\"\n",
    "            def on_log(self, args, state, control, logs=None, **kwargs):\n",
    "                if logs:\n",
    "                    print(f\"Step {state.global_step}: {logs}\")\n",
    "\n",
    "        trainer = Trainer(\n",
    "            model=model,\n",
    "            train_dataset=dataset,\n",
    "            args=TrainingArguments(\n",
    "                per_device_train_batch_size=2,\n",
    "                gradient_accumulation_steps=4,  # 실제 배치 크기 = 2 * 4 = 8\n",
    "                warmup_steps=5,\n",
    "                max_steps=60,  # 빠른 테스트를 위한 스텝 수\n",
    "                learning_rate=2e-4,\n",
    "                bf16=True,  # bfloat16 사용으로 메모리 절약\n",
    "                logging_steps=1,\n",
    "                weight_decay=0.01,\n",
    "                lr_scheduler_type=\"linear\",\n",
    "                seed=1234,\n",
    "                output_dir=\"outputs\",\n",
    "                report_to=\"none\"  # 외부 로깅 서비스 사용 안 함\n",
    "            ),\n",
    "            callbacks=[TrainingCallback()],\n",
    "        )\n",
    "\n",
    "        # GPU 메모리 상태 확인\n",
    "        gpu_stats = torch.cuda.get_device_properties(0)\n",
    "        start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024**3, 3)\n",
    "        max_memory = round(gpu_stats.total_memory / 1024**3, 3)\n",
    "        print(f\"GPU: {gpu_stats.name}, Max memory: {max_memory} GB\")\n",
    "        print(f\"Initial reserved memory: {start_gpu_memory} GB\")\n",
    "        \n",
    "        # 5. 학습 실행\n",
    "        print(\"=\" * 80)\n",
    "        print(\"Step 5: Starting Training\")\n",
    "        print(\"=\" * 80)\n",
    "        \n",
    "        trainer_stats = trainer.train()\n",
    "        \n",
    "        # 학습 완료 후 메모리 및 시간 통계 출력\n",
    "        used_memory = round(torch.cuda.max_memory_reserved() / 1024**3, 3)\n",
    "        used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n",
    "        used_percentage = round(used_memory / max_memory * 100, 3)\n",
    "        lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n",
    "        \n",
    "        print(\"=\" * 80)\n",
    "        print(\"Training Statistics\")\n",
    "        print(\"=\" * 80)\n",
    "        print(f\"Training time: {trainer_stats.metrics['train_runtime']:.2f} seconds ({trainer_stats.metrics['train_runtime']/60:.2f} minutes)\")\n",
    "        print(f\"Peak reserved memory: {used_memory} GB ({used_percentage}% of max)\")\n",
    "        print(f\"Memory for training: {used_memory_for_lora} GB ({lora_percentage}% of max)\")\n",
    "        \n",
    "        # 6. 모델 저장\n",
    "        print(\"=\" * 80)\n",
    "        print(\"Step 6: Saving Model and Tokenizer\")\n",
    "        print(\"=\" * 80)\n",
    "        \n",
    "        model_dir = f\"{pvc_data_path}/kanana-2-1b-kcdocs\"\n",
    "        \n",
    "        # LoRA 가중치를 기본 모델과 병합 후 저장\n",
    "        model = model.merge_and_unload()\n",
    "        model.save_pretrained(model_dir)\n",
    "        tokenizer.save_pretrained(model_dir)\n",
    "        \n",
    "        print(f\"Model and tokenizer saved to: {model_dir}\")\n",
    "        \n",
    "        # 7. 분산 학습 프로세스 그룹 정리 (경고 방지)\n",
    "        print(\"=\" * 80)\n",
    "        print(\"Step 7: Cleaning up distributed process group\")\n",
    "        print(\"=\" * 80)\n",
    "        \n",
    "        if torch.distributed.is_initialized():\n",
    "            torch.distributed.destroy_process_group()\n",
    "            print(\"Distributed process group destroyed successfully\")\n",
    "        else:\n",
    "            print(\"No distributed process group to clean up\")\n",
    "        \n",
    "        # 8. CUDA 컨텍스트 및 리소스 정리\n",
    "        print(\"=\" * 80)\n",
    "        print(\"Step 8: Cleaning up CUDA resources\")\n",
    "        print(\"=\" * 80)\n",
    "        \n",
    "        # CUDA 캐시 정리\n",
    "        if torch.cuda.is_available():\n",
    "            torch.cuda.empty_cache()\n",
    "            torch.cuda.synchronize()\n",
    "            print(\"CUDA cache cleared and synchronized\")\n",
    "        \n",
    "        # 모델과 토크나이저를 메모리에서 해제\n",
    "        del model\n",
    "        del tokenizer\n",
    "        del base_model\n",
    "        import gc\n",
    "        gc.collect()\n",
    "        if torch.cuda.is_available():\n",
    "            torch.cuda.empty_cache()\n",
    "        print(\"Model and tokenizer released from memory\")\n",
    "        \n",
    "        # 9. 명시적으로 프로세스 종료 (컨테이너가 종료되도록)\n",
    "        print(\"=\" * 80)\n",
    "        print(\"Step 9: Training completed successfully, exiting...\")\n",
    "        print(\"=\" * 80)\n",
    "    \n",
    "    # CustomTrainer 설정\n",
    "    # PVC 마운트 경로\n",
    "    pvc_data_path = \"/data\"\n",
    "    \n",
    "    # 사용할 LLM 모델명 (HuggingFace 모델 ID)\n",
    "    llm_model_name = \"kakaocorp/kanana-nano-2.1b-base\"\n",
    "    \n",
    "    # CustomTrainer 생성\n",
    "    # resources_per_node는 limits와 requests를 동일하게 설정합니다\n",
    "    trainer = CustomTrainer(\n",
    "        func=finetune_kanana,\n",
    "        func_args={\n",
    "            \"model_name\": llm_model_name,\n",
    "            \"epoch_num\": epoch_num,\n",
    "            \"pvc_data_path\": pvc_data_path\n",
    "        },\n",
    "        num_nodes=1,\n",
    "        resources_per_node={\n",
    "            \"nvidia.com/gpu\": \"1\",  # GPU 1개\n",
    "            \"cpu\": \"8\",              # CPU 8코어\n",
    "            \"memory\": \"16Gi\"         # 메모리 16GB\n",
    "        },\n",
    "        # TrainJob Pod에서 학습 함수 실행 전에 자동 설치될 패키지 목록\n",
    "        packages_to_install=[\n",
    "            \"transformers\",\n",
    "            \"peft\",              # PEFT 라이브러리 (LoRA 파인튜닝용)\n",
    "            \"datasets\",          # 데이터셋 로드용\n",
    "            \"torch\",             # PyTorch\n",
    "            \"pandas\",           # CSV 데이터 로드용\n",
    "            \"accelerate>=0.26.0\",  # Trainer가 요구하는 패키지\n",
    "        ],\n",
    "    )\n",
    "    \n",
    "    # TrainerClient 초기화\n",
    "    trainer_client = TrainerClient()\n",
    "    \n",
    "    # TrainJob 생성 옵션 설정\n",
    "    train_kwargs = {\"trainer\": trainer}\n",
    "    \n",
    "    # PVC 마운트를 위한 PodTemplateOverrides 설정\n",
    "    if pvc_name:\n",
    "        try:\n",
    "            from kubeflow.trainer.options.kubernetes import (\n",
    "                PodTemplateOverrides,\n",
    "                PodTemplateOverride,\n",
    "                PodSpecOverride,\n",
    "                ContainerOverride,\n",
    "            )\n",
    "            \n",
    "            options_list = [\n",
    "                PodTemplateOverrides(\n",
    "                    PodTemplateOverride(\n",
    "                        target_jobs=[\"node\"],\n",
    "                        metadata={\n",
    "                            \"annotations\": {\n",
    "                                \"sidecar.istio.io/inject\": \"false\"\n",
    "                            }\n",
    "                        },\n",
    "                        spec=PodSpecOverride(\n",
    "                            volumes=[\n",
    "                                {\n",
    "                                    \"name\": \"data\",\n",
    "                                    \"persistentVolumeClaim\": {\"claimName\": pvc_name}\n",
    "                                }\n",
    "                            ],\n",
    "                            containers=[\n",
    "                                ContainerOverride(\n",
    "                                    name=\"node\",\n",
    "                                    volume_mounts=[\n",
    "                                        {\n",
    "                                            \"name\": \"data\",\n",
    "                                            \"mountPath\": pvc_data_path\n",
    "                                        }\n",
    "                                    ]\n",
    "                                )\n",
    "                            ]\n",
    "                        )\n",
    "                    )\n",
    "                )\n",
    "            ]\n",
    "            train_kwargs[\"options\"] = options_list\n",
    "        except ImportError:\n",
    "            print(\"Warning: Could not import PodTemplateOverrides. PVC mounting may not work.\")\n",
    "    \n",
    "    # TrainJob 생성\n",
    "    print(\"Creating TrainJob...\")\n",
    "    import inspect\n",
    "    sig = inspect.signature(trainer_client.train)\n",
    "    \n",
    "    if 'runtime' in sig.parameters and sig.parameters['runtime'].default != inspect.Parameter.empty:\n",
    "        job_id = trainer_client.train(**train_kwargs)\n",
    "    else:\n",
    "        try:\n",
    "            job_id = trainer_client.create_trainjob(trainer)\n",
    "        except AttributeError:\n",
    "            raise RuntimeError(\n",
    "                \"TrainJob creation requires runtime configuration. \"\n",
    "                \"Please ensure ClusterTrainingRuntime is properly configured.\"\n",
    "            )\n",
    "    \n",
    "    print(f\"TrainJob created with job_id: {job_id}\")\n",
    "    \n",
    "    # TrainJob 시작 대기\n",
    "    print(\"Waiting for TrainJob to start...\")\n",
    "    time.sleep(10)\n",
    "    \n",
    "    # TrainJob 로그 확인\n",
    "    print(\"\\n=== TrainJob Logs ===\")\n",
    "    try:\n",
    "        logs = list(trainer_client.get_job_logs(job_id, follow=False))\n",
    "        if logs:\n",
    "            for logline in logs:\n",
    "                print(logline)\n",
    "        else:\n",
    "            print(\"No logs available yet\")\n",
    "    except Exception as log_error:\n",
    "        print(f\"Warning: Could not retrieve logs: {log_error}\")\n",
    "    \n",
    "    print(\"\\nTraining job submitted successfully!\")\n",
    "    \n",
    "    # job_id를 컴포넌트 출력으로 반환\n",
    "    with open(train_job_id.path, 'w') as f:\n",
    "        f.write(job_id)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3772023a-8047-4c23-b2ee-32e307c9cdc3",
   "metadata": {},
   "source": [
    "### 3. 모델 서빙 컴포넌트\n",
    "\n",
    "KServe InferenceService를 생성하여 파인튜닝된 모델을 vLLM으로 서빙하는 컴포넌트입니다.\n",
    "- vLLM 백엔드를 사용한 고성능 추론\n",
    "- PVC에서 모델 로드\n",
    "- GPU 리소스 자동 할당"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 191,
   "id": "71b2300d-0f9d-4a5d-98c2-7f92c05187e1",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/tmp/ipykernel_525/4282835763.py:1: DeprecationWarning: output_component_file parameter is deprecated and will eventually be removed. Please use `Compiler().compile()` to compile a component instead.\n",
      "  @dsl.component(\n"
     ]
    }
   ],
   "source": [
    "@dsl.component(\n",
    "    packages_to_install=['kubeflow', 'kubernetes', 'pyyaml'],\n",
    "    install_kfp_package=True,\n",
    "    base_image='python:3.11',\n",
    "    output_component_file=f'{SERVE_ENPOINT_PATH}/deploy_component.yaml'\n",
    ")\n",
    "def deploy_kanana_op_func(\n",
    "    namespace: str,\n",
    "    pvc_name: str,\n",
    "    kserve_name: str,\n",
    "    train_job_id: dsl.Input[dsl.Artifact],\n",
    "    model_path_in_pvc: str = \"kanana-2-1b-kcdocs\",\n",
    "    served_model_name: str = \"kanana-nano-2.1b-base\",\n",
    "    max_model_len: str = \"32768\",\n",
    "    gpu_memory_utilization: str = \"0.8\",\n",
    "):\n",
    "    \"\"\"\n",
    "    KServe InferenceService를 생성하여 파인튜닝된 모델을 vLLM으로 배포\n",
    "    \n",
    "    Args:\n",
    "        namespace: Kubernetes 네임스페이스\n",
    "        pvc_name: 모델이 저장된 PVC 이름\n",
    "        kserve_name: InferenceService 이름\n",
    "        train_job_id: TrainJob ID (이전 컴포넌트에서 전달받음)\n",
    "        model_path_in_pvc: PVC 내 모델 경로\n",
    "        served_model_name: 서빙될 모델 이름\n",
    "        max_model_len: 최대 시퀀스 길이\n",
    "        gpu_memory_utilization: GPU 메모리 사용률 (0.0-1.0)\n",
    "    \"\"\"\n",
    "    from kubernetes import client, config\n",
    "    from kubernetes.config import ConfigException\n",
    "    from kubeflow.trainer import TrainerClient\n",
    "    import time\n",
    "    import sys\n",
    "    \n",
    "    # Kubernetes 클라이언트 초기화\n",
    "    try:\n",
    "        config.load_incluster_config()  # Pod 내부에서 실행 시\n",
    "    except ConfigException:\n",
    "        config.load_kube_config()  # 로컬에서 실행 시\n",
    "    \n",
    "    custom_api = client.CustomObjectsApi()\n",
    "    group = \"serving.kserve.io\"\n",
    "    version = \"v1beta1\"\n",
    "    plural = \"inferenceservices\"\n",
    "    \n",
    "    # KFP v2에서 아티팩트는 파일 경로를 통해 접근합니다\n",
    "    # 아티팩트 파일에서 실제 job_id 문자열을 읽어옵니다\n",
    "    with open(train_job_id.path, 'r') as f:\n",
    "        trainjob_id_str = f.read().strip()\n",
    "    \n",
    "    # TrainerClient 초기화\n",
    "    trainer_client = TrainerClient()\n",
    "    \n",
    "    # polling until trainjob is done\n",
    "    max_wait = 60 * 60  # 1 hour at most\n",
    "    interval = 15       # seconds\n",
    "    waited = 0\n",
    "\n",
    "    print(f\"Waiting for TrainJob '{trainjob_id_str}' to complete...\")\n",
    "\n",
    "    while waited < max_wait:\n",
    "        try:\n",
    "            trainjob = trainer_client.get_job(trainjob_id_str)\n",
    "            job_status = trainjob.status if hasattr(trainjob, 'status') else None\n",
    "            \n",
    "            if job_status == 'Complete':\n",
    "                print(f\"TrainJob '{trainjob_id_str}' completed successfully.\")\n",
    "                break\n",
    "            \n",
    "            if job_status == 'Failed':\n",
    "                raise RuntimeError(f\"TrainJob '{trainjob_id_str}' failed!\")\n",
    "            \n",
    "        except RuntimeError:\n",
    "            raise\n",
    "        except Exception as e:\n",
    "            # TrainJob이 아직 생성되지 않았거나 다른 에러인 경우 계속 대기\n",
    "            pass\n",
    "        \n",
    "        time.sleep(interval)\n",
    "        waited += interval\n",
    "    else:\n",
    "        raise TimeoutError(f\"Timed out waiting for TrainJob '{trainjob_id_str}' to complete.\")\n",
    "\n",
    "    # InferenceService 매니페스트 정의\n",
    "    # vLLM 백엔드를 사용하여 HuggingFace 형식 모델을 서빙\n",
    "    inferenceservice_manifest = {\n",
    "        \"apiVersion\": f\"{group}/{version}\",\n",
    "        \"kind\": \"InferenceService\",\n",
    "        \"metadata\": {\n",
    "            \"name\": kserve_name,\n",
    "            \"namespace\": namespace,\n",
    "        },\n",
    "        \"spec\": {\n",
    "            \"predictor\": {\n",
    "                \"annotations\": {\n",
    "                    \"serving.knative.dev/progress-deadline\": \"1h\"\n",
    "                },\n",
    "                \"automountServiceAccountToken\": False,\n",
    "            \"maxReplicas\": 1,\n",
    "            \"minReplicas\": 1,\n",
    "            \"model\": {\n",
    "                \"args\": [\n",
    "                    f\"--model_name={served_model_name}\",\n",
    "                    \"--model_id=/mnt/models\",\n",
    "                    \"--dtype=bfloat16\",\n",
    "                    \"--backend=vllm\"\n",
    "                ],\n",
    "                    \"env\": [\n",
    "                        {\n",
    "                            \"name\": \"VLLM_LOGGING_LEVEL\",\n",
    "                            \"value\": \"DEBUG\"\n",
    "                        },\n",
    "                        {\n",
    "                            \"name\": \"MAX_MODEL_LEN\",\n",
    "                            \"value\": max_model_len\n",
    "                        },\n",
    "                        {\n",
    "                            \"name\": \"GPU_MEMORY_UTILIZATION\",\n",
    "                            \"value\": gpu_memory_utilization\n",
    "                        },\n",
    "                        {\n",
    "                            \"name\": \"PYTORCH_CUDA_ALLOC_CONF\",\n",
    "                            \"value\": \"expandable_segments:True,max_split_size_mb:128\"\n",
    "                        }\n",
    "                    ],\n",
    "                    \"lifecycle\": {\n",
    "                        \"preStop\": {\n",
    "                            \"exec\": {\n",
    "                                \"command\": [\"\"]\n",
    "                            }\n",
    "                        }\n",
    "                    },\n",
    "                    \"modelFormat\": {\n",
    "                        \"name\": \"huggingface\"\n",
    "                    },\n",
    "                    \"name\": \"\",\n",
    "                    \"resources\": {\n",
    "                        \"limits\": {\n",
    "                            \"cpu\": \"23\",\n",
    "                            \"memory\": \"180Gi\",\n",
    "                            \"nvidia.com/gpu\": \"1\"\n",
    "                        },\n",
    "                        \"requests\": {\n",
    "                            \"cpu\": \"1\",\n",
    "                            \"memory\": \"2Gi\",\n",
    "                            \"nvidia.com/gpu\": \"1\"\n",
    "                        }\n",
    "                    },\n",
    "                    \"storageUri\": f\"pvc://{pvc_name}/{model_path_in_pvc}\"\n",
    "                },\n",
    "                \"timeout\": 600\n",
    "            }\n",
    "        }\n",
    "    }\n",
    "    \n",
    "    # InferenceService 생성 또는 업데이트\n",
    "    try:\n",
    "        # 기존 InferenceService 존재 여부 확인\n",
    "        try:\n",
    "            existing_isvc = custom_api.get_namespaced_custom_object(\n",
    "                group=group,\n",
    "                version=version,\n",
    "                namespace=namespace,\n",
    "                plural=plural,\n",
    "                name=kserve_name\n",
    "            )\n",
    "            print(f\"InferenceService '{kserve_name}' already exists. Updating...\")\n",
    "            \n",
    "            # 기존 리소스의 metadata와 spec을 업데이트\n",
    "            inferenceservice_manifest[\"metadata\"][\"resourceVersion\"] = existing_isvc[\"metadata\"].get(\"resourceVersion\")\n",
    "            \n",
    "            # InferenceService 업데이트\n",
    "            updated_isvc = custom_api.patch_namespaced_custom_object(\n",
    "                group=group,\n",
    "                version=version,\n",
    "                namespace=namespace,\n",
    "                plural=plural,\n",
    "                name=kserve_name,\n",
    "                body=inferenceservice_manifest\n",
    "            )\n",
    "            print(f\"InferenceService '{kserve_name}' updated successfully\")\n",
    "            \n",
    "        except client.rest.ApiException as e:\n",
    "            if e.status == 404:\n",
    "                # InferenceService가 존재하지 않으면 생성\n",
    "                print(f\"Creating InferenceService '{kserve_name}'...\")\n",
    "                created_isvc = custom_api.create_namespaced_custom_object(\n",
    "                    group=group,\n",
    "                    version=version,\n",
    "                    namespace=namespace,\n",
    "                    plural=plural,\n",
    "                    body=inferenceservice_manifest\n",
    "                )\n",
    "                print(f\"InferenceService '{kserve_name}' created successfully\")\n",
    "            else:\n",
    "                raise\n",
    "        \n",
    "        # InferenceService가 Ready 상태가 될 때까지 대기\n",
    "        print(f\"Waiting for InferenceService '{kserve_name}' to be ready...\")\n",
    "        max_wait_time = 1800  # 30분\n",
    "        wait_interval = 10  # 10초마다 체크\n",
    "        elapsed_time = 0\n",
    "        \n",
    "        while elapsed_time < max_wait_time:\n",
    "            try:\n",
    "                isvc_status = custom_api.get_namespaced_custom_object_status(\n",
    "                    group=group,\n",
    "                    version=version,\n",
    "                    namespace=namespace,\n",
    "                    plural=plural,\n",
    "                    name=kserve_name\n",
    "                )\n",
    "                \n",
    "                # 상태 확인\n",
    "                conditions = isvc_status.get(\"status\", {}).get(\"conditions\", [])\n",
    "                ready = False\n",
    "                \n",
    "                for condition in conditions:\n",
    "                    if condition.get(\"type\") == \"Ready\":\n",
    "                        if condition.get(\"status\") == \"True\":\n",
    "                            ready = True\n",
    "                            break\n",
    "                        elif condition.get(\"status\") == \"False\":\n",
    "                            reason = condition.get(\"reason\", \"Unknown\")\n",
    "                            message = condition.get(\"message\", \"\")\n",
    "                            print(f\"InferenceService not ready. Reason: {reason}, Message: {message}\")\n",
    "                \n",
    "                if ready:\n",
    "                    print(f\"InferenceService '{kserve_name}' is ready!\")\n",
    "                    break\n",
    "                \n",
    "                time.sleep(wait_interval)\n",
    "                elapsed_time += wait_interval\n",
    "                print(f\"Still waiting... ({elapsed_time}/{max_wait_time} seconds)\")\n",
    "                \n",
    "            except Exception as status_error:\n",
    "                print(f\"Error checking status: {status_error}\")\n",
    "                time.sleep(wait_interval)\n",
    "                elapsed_time += wait_interval\n",
    "        \n",
    "        if elapsed_time >= max_wait_time:\n",
    "            print(f\"Warning: InferenceService '{kserve_name}' did not become ready within {max_wait_time} seconds\")\n",
    "        \n",
    "        print(f\"KServe InferenceService '{kserve_name}' deployment completed successfully\")\n",
    "        return\n",
    "        \n",
    "    except Exception as e:\n",
    "        print(f\"Error deploying InferenceService: {e}\")\n",
    "        import traceback\n",
    "        print(f\"Traceback: {traceback.format_exc()}\")\n",
    "        raise\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a92c3139-96a1-4f60-a300-691f130595f7",
   "metadata": {},
   "source": [
    "## 파이프라인 정의 및 실행\n",
    "\n",
    "세 개의 컴포넌트를 연결하여 전체 파이프라인을 구성합니다.\n",
    "1. PVC 생성 → 데이터 다운로드 → 모델 파인튜닝 → 모델 서빙"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c5e95b81-f456-448f-9d28-579460b84af0",
   "metadata": {},
   "outputs": [],
   "source": [
    "@dsl.pipeline(name=\"Kanana Model Finetuning Pipeline\")\n",
    "def kanana_model_finetuning_Pipeline(\n",
    "    kc_kbm_os_train_url: str = 'https://objectstorage.kr-central-2.kakaocloud.com/v1/c11fcba415bd4314b595db954e4d4422/public/tutorial/kubeflow/kubeflow-tensorboard/data/sample_train_data.csv',\n",
    "    epoch_num: str = \"10\",\n",
    "    job_name: str = None,\n",
    "    endpoint_name: str = None,\n",
    "):\n",
    "    \"\"\"\n",
    "    Kanana 모델 파인튜닝 및 서빙 파이프라인\n",
    "    \n",
    "    Args:\n",
    "        kc_kbm_os_train_url: 학습 데이터 CSV 파일의 Object Storage URL\n",
    "        epoch_num: 학습 에포크 수\n",
    "        job_name: TrainJob 이름 (선택적)\n",
    "    \"\"\"\n",
    "    # 1. PVC 생성 (데이터 및 모델 저장용)\n",
    "    pvc1 = kubernetes.CreatePVC(\n",
    "        pvc_name=PVC_NAME,\n",
    "        access_modes=['ReadWriteMany'],\n",
    "        size='10Gi',\n",
    "        storage_class_name='',\n",
    "    )\n",
    "    \n",
    "    # 2. 데이터 다운로드 컴포넌트\n",
    "    download_data = download_dataset(kc_kbm_os_train_url=kc_kbm_os_train_url)\n",
    "    download_data.set_cpu_request(cpu=\"1\").set_memory_request(memory=\"2G\")\n",
    "    download_data.set_caching_options(enable_caching=False)\n",
    "    \n",
    "    # PVC 마운트\n",
    "    kubernetes.mount_pvc(\n",
    "        download_data,\n",
    "        pvc_name=pvc1.outputs['name'],\n",
    "        mount_path='/data',\n",
    "    )\n",
    "    \n",
    "    # 3. 모델 파인튜닝 컴포넌트\n",
    "    model_train = finetune_kanana_model(\n",
    "        epoch_num=epoch_num,\n",
    "        namespace=KBM_NAMESPACE,\n",
    "        pvc_name=pvc1.outputs['name'],\n",
    "        job_name=job_name,\n",
    "    )\n",
    "    model_train.set_cpu_request(cpu=\"1\").set_memory_request(memory=\"2G\")\n",
    "    # .set_cpu_limit(cpu=\"1\").set_memory_limit(memory=\"2G\")\n",
    "    model_train.set_caching_options(enable_caching=False)\n",
    "    model_train.after(download_data)  # 데이터 다운로드 후 실행\n",
    "\n",
    "    inference_model = deploy_kanana_op_func(\n",
    "        namespace=KBM_NAMESPACE,\n",
    "        kserve_name=endpoint_name,\n",
    "        pvc_name=pvc1.outputs['name'],\n",
    "        train_job_id=model_train.output,\n",
    "        served_model_name=\"kanana-nano-2.1b-base\",\n",
    "        max_model_len=\"8192\",\n",
    "        gpu_memory_utilization=\"0.8\",\n",
    "    )\n",
    "    inference_model.set_cpu_request(cpu=\"1\").set_memory_request(memory=\"2G\")\n",
    "    # .set_cpu_limit(cpu=\"1\").set_memory_limit(memory=\"2G\")\n",
    "    inference_model.set_display_name(\"Serving Finetuned Kanana Model\")\n",
    "    inference_model.after(model_train)  # 파인튜닝 완료 후 실행\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 193,
   "id": "2baafcca-cfdf-4919-8217-8e79737180d6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<a href=\"https://nipagpu.kakaocloud.com/pipeline/?ns=kbm-g-np-nipa-test-company#/experiments/details/862c5a35-85b7-491f-909d-d9b2707ea703\" target=\"_blank\" >Experiment details</a>."
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<a href=\"https://nipagpu.kakaocloud.com/pipeline/#/runs/details/ea42d898-2cd6-4bc8-b673-9a783a83700a\" target=\"_blank\" >Run details</a>."
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Pipeline run created: ea42d898-2cd6-4bc8-b673-9a783a83700a\n"
     ]
    }
   ],
   "source": [
    "# 파이프라인 실행\n",
    "# Experiment와 Run을 생성하여 파이프라인을 실행합니다\n",
    "experiment_name = kanana_model_finetuning_Pipeline.name + ' experiment'\n",
    "run_name = kanana_model_finetuning_Pipeline.name + ' run'\n",
    "\n",
    "arguments = {\n",
    "    \"epoch_num\": str(EPOCH_NUM),\n",
    "    \"job_name\": MODEL_NAME,\n",
    "    \"endpoint_name\": KSERVE_ISVC_NAME,\n",
    "}\n",
    "\n",
    "# 파이프라인 실행\n",
    "run_result = client.create_run_from_pipeline_func(\n",
    "    kanana_model_finetuning_Pipeline, \n",
    "    experiment_name=experiment_name, \n",
    "    run_name=run_name, \n",
    "    arguments=arguments\n",
    ")\n",
    "\n",
    "print(f\"Pipeline run created: {run_result.run_id}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "90020c6a-d152-4f6f-b834-0af4b0423589",
   "metadata": {},
   "source": [
    "## 모델 서빙 API 테스트\n",
    "\n",
    "파이프라인 실행 후 InferenceService가 Ready 상태가 되면, 배포된 모델을 테스트할 수 있습니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b1d26ffc-c228-4500-85d7-7ec4f4b51098",
   "metadata": {},
   "source": [
    "### 방법 1: requests 라이브러리를 사용한 API 테스트"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 194,
   "id": "7e5ea652",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Kubeflow 인증을 위한 세션 쿠키 획득\n",
    "import requests\n",
    "\n",
    "host = os.environ.get(\"KUBEFLOW_HOST\", \"https://nipagpu.kakaocloud.com\")\n",
    "username = os.environ.get(\"KUBEFLOW_USERNAME\", \"\")\n",
    "password = os.environ.get(\"KUBEFLOW_PASSWORD\", \"\")\n",
    "\n",
    "# 인증 세션 생성\n",
    "session = requests.Session()\n",
    "_kargs = {\"verify\": False} if host.startswith(\"https\") else {}\n",
    "\n",
    "response = session.get(host, **_kargs)\n",
    "session.post(\n",
    "    response.url,\n",
    "    headers={\"Content-Type\": \"application/x-www-form-urlencoded\"},\n",
    "    data={\"login\": username, \"password\": password}\n",
    ")\n",
    "session_cookie = session.cookies.get_dict().get(\"authservice_session\", \"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 195,
   "id": "73d569a2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Ready 상태: True\n",
      "✓ InferenceService가 Ready 상태입니다.\n",
      "\n",
      "ISVC_NAME 변수에 'kanana-isvc-4471d88e'이 설정되었습니다.\n"
     ]
    }
   ],
   "source": [
    "# 가장 최근에 생성된 KServe InferenceService 가져오기\n",
    "from kubernetes import client, config\n",
    "from kubernetes.config import ConfigException\n",
    "\n",
    "# Kubernetes 클라이언트 초기화\n",
    "try:\n",
    "    config.load_incluster_config()\n",
    "except ConfigException:\n",
    "    config.load_kube_config()\n",
    "\n",
    "custom_api = client.CustomObjectsApi()\n",
    "group = \"serving.kserve.io\"\n",
    "version = \"v1beta1\"\n",
    "plural = \"inferenceservices\"\n",
    "\n",
    "# 현재 네임스페이스의 모든 InferenceService 조회\n",
    "isvcs = custom_api.list_namespaced_custom_object(\n",
    "    group=group,\n",
    "    version=version,\n",
    "    namespace=KBM_NAMESPACE,\n",
    "    plural=plural\n",
    ")\n",
    "\n",
    "# 생성 시간 기준으로 정렬하여 가장 최근 것 선택\n",
    "if isvcs.get('items'):\n",
    "    # creationTimestamp 기준으로 정렬\n",
    "    sorted_isvcs = sorted(\n",
    "        isvcs.get('items', []),\n",
    "        key=lambda x: x.get('metadata', {}).get('creationTimestamp', ''),\n",
    "        reverse=True\n",
    "    )\n",
    "    \n",
    "    latest_isvc = sorted_isvcs[0]\n",
    "    isvc_name = latest_isvc.get('metadata', {}).get('name', 'Unknown')\n",
    "    creation_time = latest_isvc.get('metadata', {}).get('creationTimestamp', 'Unknown')\n",
    "        \n",
    "    # 상태 확인\n",
    "    status = latest_isvc.get('status', {})\n",
    "    if status:\n",
    "        conditions = status.get('conditions', [])\n",
    "        for condition in conditions:\n",
    "            if condition.get('type') == 'Ready':\n",
    "                ready_status = condition.get('status', 'Unknown')\n",
    "                print(f\"Ready 상태: {ready_status}\")\n",
    "                if ready_status == 'True':\n",
    "                    print(\"✓ InferenceService가 Ready 상태입니다.\")\n",
    "                else:\n",
    "                    reason = condition.get('reason', '')\n",
    "                    message = condition.get('message', '')\n",
    "                    print(f\"⚠ InferenceService가 아직 준비되지 않았습니다.\")\n",
    "                    print(f\"  Reason: {reason}\")\n",
    "                    print(f\"  Message: {message}\")\n",
    "    \n",
    "    # 전역 변수로 설정 (다음 셀에서 사용 가능)\n",
    "    ISVC_NAME = isvc_name\n",
    "    print(f\"\\nISVC_NAME 변수에 '{isvc_name}'이 설정되었습니다.\")\n",
    "else:\n",
    "    print(\"InferenceService를 찾을 수 없습니다.\")\n",
    "    ISVC_NAME = None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 198,
   "id": "2bad68c0-7e8e-458a-a27f-b095796b8442",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "입력 프롬프트: 카카오엔터프라이즈에 대해서 설명해줘\n",
      "상태 코드: 200\n",
      "응답:   카카오 엔터프라이즈는 카카오의 계열사로, 클라우드 및 인공지능(AI) 기반의 클라우드 서비스, SaaS 기반 애플리케이션 및 서비스를 개발하고 제공하는 기업입니다. 이를 통해 다양한 산업군과 기업들이 인프라와 인공지능 서비스를 효율적으로 활용하며 혁신을 이룰 수 있도록 지원합니다. 카카오 엔터프라이즈는 지속적인 기술 혁신과 고객 중심의 전략을 통해 더 나은 서비스를 제공하기 위해 노력하고 있습니다.\n"
     ]
    }
   ],
   "source": [
    "# OpenAI API 형식으로 InferenceService 테스트\n",
    "NAMESPACE = KBM_NAMESPACE\n",
    "KUBEFLOW_PUBLIC_DOMAIN = host.split(\"//\")[1]\n",
    "SERVED_MODEL_NAME = \"kanana-nano-2.1b-base\"\n",
    "\n",
    "# 테스트 프롬프트\n",
    "prompt_text = \"카카오엔터프라이즈에 대해서 설명해줘\"\n",
    "\n",
    "# OpenAI completions API 형식 요청\n",
    "data = {\n",
    "    \"model\": SERVED_MODEL_NAME,\n",
    "    \"prompt\": prompt_text,\n",
    "    \"stream\": False,\n",
    "    \"max_tokens\": 1000\n",
    "}\n",
    "\n",
    "# API 요청\n",
    "response = requests.post(\n",
    "    url=f\"{host}/openai/v1/completions\",\n",
    "    cookies={'authservice_session': session_cookie},\n",
    "    headers={\n",
    "        \"Host\": f\"{ISVC_NAME}.{NAMESPACE}.{KUBEFLOW_PUBLIC_DOMAIN}\",\n",
    "        \"Content-Type\": \"application/json\",\n",
    "    },\n",
    "    json=data,\n",
    "    **_kargs\n",
    ")\n",
    "\n",
    "response_json = response.json()\n",
    "\n",
    "print(f\"입력 프롬프트: {prompt_text}\")\n",
    "print(f\"상태 코드: {response.status_code}\")\n",
    "print(f\"응답: {response_json['choices'][0]['text']}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "312a3356-3987-4782-b7e5-184fc4513e11",
   "metadata": {},
   "source": [
    "### 방법 2: LangChain을 사용한 API 테스트\n",
    "\n",
    "LangChain의 ChatOpenAI를 사용하여 InferenceService와 통신할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 199,
   "id": "29c2a868",
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# LangChain OpenAI 패키지 설치 (필요한 경우)\n",
    "# !pip install langchain-openai"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 203,
   "id": "46b3309b-7e2f-4c5a-b3e2-effe4193daa3",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:httpx:HTTP Request: POST http://kanana-isvc-4471d88e.kbm-g-np-nipa-test-company.svc.cluster.local/openai/v1/chat/completions \"HTTP/1.1 200 OK\"\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "카카오엔터프라이즈는 한국의 대표적인 IT 기업인 카카오를 기반으로 하는 기업으로, 인공지능(AI) 및 소프트웨어 개발 등 IT 기술과 관련된 다양한 제품과 서비스를 제공합니다.\n"
     ]
    }
   ],
   "source": [
    "# LangChain을 사용한 테스트 (선택적)\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# InferenceService 내부 서비스 URL (클러스터 내부에서 접근)\n",
    "llm_svc_url = f\"http://{ISVC_NAME}.{NAMESPACE}.svc.cluster.local/\"\n",
    "\n",
    "llm = ChatOpenAI(\n",
    "    model_name=SERVED_MODEL_NAME,\n",
    "    base_url=f\"{llm_svc_url}openai/v1\",\n",
    "    openai_api_key=\"empty\"  # KServe는 API 키를 요구하지 않음\n",
    ")\n",
    "\n",
    "input_text = \"카카오엔터프라이즈에 대해서 설명해줘\"\n",
    "result = llm.invoke(input_text)\n",
    "print(result.content)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c3c0a229-e968-46bf-a43c-3c596f741720",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
