> For the complete documentation index, see [llms.txt](https://aurimrv.gitbook.io/pratica-devops-com-docker-para-machine-learning/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://aurimrv.gitbook.io/pratica-devops-com-docker-para-machine-learning/id-11-mlops-com-kubeflow/11.4-kserve.md).

# 11.4 Kserve

O [KServe](https://kserve.github.io/website/0.15/) habilita a inferência de modelos de Machine Learning no Kubernetes e é totalmente integrado ao Kubeflow. A seguir, vamos portar o modelo de classificação de categorias visto nos capítulos anteriores para o KServe. Antes de começar os trabalhos com o modelo, precisamos configurar as credenciais para que o KServe consiga obter os artefatos no Minio (ou SeaweedFS).

> \[!NOTE] No Kubeflow v1.11.0, o **SeaweedFS** é o backend de armazenamento S3 padrão. Por isso, definimos o endpoint como `seaweedfs-s3.kubeflow:8333` no Secret abaixo e nos códigos Python a seguir. Caso queira usar um MinIO legado ou customizado, basta alterar o endpoint para `minio-service.kubeflow:9000` (além de ajustar as credenciais de acesso se necessário).

Para usar o armazenamento S3-compatible, vamos criar uma **Secret** com as credenciais necessárias e um **Service Account** para utilizarmos na subida do serviço:

```bash
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: s3creds
  namespace: kubeflow-user-example-com
  annotations:
    serving.kserve.io/s3-endpoint: "seaweedfs-s3.kubeflow:8333" # Mudar para minio-service.kubeflow:9000 se usar MinIO legado
    serving.kserve.io/s3-usehttps: "0"
    serving.kserve.io/s3-region: "us-east-1"
    serving.kserve.io/s3-useanoncredential: "false"
type: Opaque
stringData:
  AWS_ACCESS_KEY_ID: "minio"
  AWS_SECRET_ACCESS_KEY: "minio123"
  S3_USE_HTTPS: "0"
  S3_VERIFY_SSL: "0"
  S3_ENDPOINT: "seaweedfs-s3.kubeflow:8333" # Mudar para minio-service.kubeflow:9000 se usar MinIO legado
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kserve-sa
  namespace: kubeflow-user-example-com
secrets:
  - name: s3creds
EOF
```

Execute o comando acima para criar a Secret e a ServiceAccount diretamente no Kubernetes.

Agora vamos voltar ao Notebook para treinar e efetuar o deploy do modelo. O comando a seguir prepara o cliente do KServe e do Minio.

```python
!pip install --upgrade pip
!pip install kserve==0.15.2 pydantic minio typing-extensions -U
```

> \[!TIP] **Resolução de Conflito de Biblioteca (typing\_extensions):** Em certos ambientes de notebook (como o JupyterHub padrão do Kubeflow), um bug de empacotamento pode manter o arquivo `/opt/conda/lib/python3.11/site-packages/typing_extensions.py` (antigo) junto da pasta `typing_extensions/` da versão atualizada. Como o Python prioriza arquivos `.py` em relação a pastas, o carregamento falha com o erro `ImportError: cannot import name 'Sentinel'`.
>
> Para solucionar isso de forma definitiva, remova o arquivo órfão e force a reinstalação executando no notebook:
>
> ```python
> !rm -f /opt/conda/lib/python3.11/site-packages/typing_extensions.py
> %pip install typing-extensions --force-reinstall
> # Após rodar, reinicie o Kernel do Jupyter.
> ```

```python
from kubernetes import client
from kserve import KServeClient
from kserve import constants
from kserve import utils
from kserve import V1beta1InferenceService
from kserve import V1beta1InferenceServiceSpec
from kserve import V1beta1PredictorSpec
from kserve import V1beta1SKLearnSpec
from minio import Minio
```

Agora, vamos inicializar algumas variáveis de credenciais do Minio, caminhos (paths) para os modelos e o namespace padrão:

```python
namespace = utils.get_default_target_namespace()
# MINIO_HOST será definido pelo bloco de diagnóstico de DNS abaixo
# Se o serviço não existir ou o DNS não resolver, troque para o MinIO legado:
# MINIO_HOST="minio-service.kubeflow:9000"

# Diagnóstico de DNS para o endpoint MinIO com fallback
import socket, sys
try:
    ip = socket.gethostbyname("seaweedfs.kubeflow.svc.cluster.local")
    print(f"✅ Resolved seaweedfs.kubeflow.svc.cluster.local to {ip}")
    MINIO_HOST = "seaweedfs.kubeflow.svc.cluster.local:8333"
except socket.gaierror as e:
    print(f"⚠️ DNS resolution failed: {e}. Usando MinIO legado.", file=sys.stderr)
    MINIO_HOST = "minio-service.kubeflow:9000"

MINIO_ACCESS_KEY="minio"
MINIO_SECRET_KEY="minio123"
MINIO_MODEL_BUCKET="kserve"
MINIO_DATA_PRODUCT_PATH="data/produtos.csv"
INCOME_MODEL_PATH="sklearn/income/model"
DEPLOY_NAMESPACE=namespace
```

Para iniciar o cliente do Minio, precisamos inicializá-lo com as credenciais:

```python
minioClient = Minio(MINIO_HOST,
  access_key=MINIO_ACCESS_KEY,
  secret_key=MINIO_SECRET_KEY,
  secure=False)
```

Para armazenar o modelo, vamos criar um bucket no Minio:

```python
if not minioClient.bucket_exists(MINIO_MODEL_BUCKET):
    minioClient.make_bucket(MINIO_MODEL_BUCKET)
```

Vamos efetuar o upload do arquivo de dados no Minio para ser a entrada de dados para o treinamento do modelo. Antes de executar o comando abaixo, faça o upload do arquivo para o seu Notebook:

```python
minioClient.fput_object(MINIO_MODEL_BUCKET,MINIO_DATA_PRODUCT_PATH,"produtos.csv")
```

Agora, vamos instalar as dependências para o treinamento do modelo:

```python
!pip install --upgrade  nltk==3.7 scikit-learn==1.1.3 joblib==1.1.0 executing==1.1.1 stack-data==0.5.1 ipython==8.22.2
```

A primeira etapa é a preparação dos dados:

```python
import pandas as pd
import nltk
from nltk.corpus import stopwords

nltk.download('stopwords')

obj = minioClient.get_object(
    MINIO_MODEL_BUCKET,
    MINIO_DATA_PRODUCT_PATH,
)
products_data = pd.read_csv(obj,delimiter=';', encoding='utf-8')
products_data.count()
products_data["informacao"] = products_data["nome"]+ products_data["descricao"]
# concatenando as colunas nome e descricao
products_data['informacao'] = products_data['nome'] + products_data['descricao']
# excluindo linhas com valor de informacao ou categoria NaN
products_data.dropna(subset=['informacao', 'categoria'], inplace=True)
products_data.drop(columns=['nome', 'descricao'], inplace=True)

stop_words=set(stopwords.words("portuguese"))
# transforma a string em caixa baixa e remove stopwords
products_data['sem_stopwords'] = products_data['informacao'].str.lower().apply(lambda x: ' '.join([word for word in x.split() if word not in (stop_words)]))
tokenizer = nltk.RegexpTokenizer(r"\w+")
products_data['tokens'] = products_data['sem_stopwords'].apply(tokenizer.tokenize) # aplica o regex tokenizer
products_data.drop(columns=['sem_stopwords','informacao'],inplace=True) # Exclui as colunas antigas

products_data["strings"]= products_data["tokens"].str.join(" ") # reunindo cada elemento da lista
products_data.head()
```

A segunda etapa é o treinamento do modelo:

```python
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline

X_train,X_test,y_train,y_test = train_test_split( # Separação dos dados para teste e treino
    products_data["strings"],
    products_data["categoria"],
    test_size = 0.2,
    random_state = 10
)
pipe = Pipeline([('vetorizador', CountVectorizer()), ("classificador", MultinomialNB())]) # novo
```

Salvar o modelo no Minio:

```python
import joblib
pipe.fit(X_train, y_train)
joblib.dump(pipe, "model.joblib")
print(minioClient.fput_object(MINIO_MODEL_BUCKET, f"{INCOME_MODEL_PATH}/model.joblib", 'model.joblib'))
```

```python
from sklearn.metrics import accuracy_score
from sklearn import metrics

y_prediction = pipe.predict(X_test)
accuracy = accuracy_score(y_prediction, y_test)
```

Agora, vamos usar o cliente do KServe para efetuar o deploy do modelo:

```python
name='sklearn-produtos'
kserve_version='v1beta1'
api_version = constants.KSERVE_GROUP + '/' + kserve_version

predictor = V1beta1PredictorSpec(
    service_account_name='kserve-sa',
    min_replicas=1,
    sklearn=V1beta1SKLearnSpec(
        # O storage_uri deve apontar para o diretório que contém model.joblib, não para o arquivo em si
        storage_uri='s3://'+MINIO_MODEL_BUCKET+'/'+ INCOME_MODEL_PATH,
        resources=client.V1ResourceRequirements(
            requests={"cpu": "300m", "memory": "128Mi"},
            limits={"cpu": "500m", "memory": "512Mi"},
        ),
    ),
)
isvc = V1beta1InferenceService(
    api_version=api_version,
    kind="InferenceService",
    metadata=client.V1ObjectMeta(
        name=name,
        namespace=DEPLOY_NAMESPACE
    ),
    spec=V1beta1InferenceServiceSpec(
        predictor=predictor
    )
)
KServe = KServeClient()
KServe.create(isvc)
```

Após alguns minutos, é possível consultar o servidor de inferência na interface do Kubeflow.

```python
KServe.get(name, namespace=namespace, watch=True, timeout_seconds=120)
```

É possível testar a chamada do endpoint de inferência via Notebook. Mas, antes de testar o endpoint, é necessário configurar o RBAC para acesso. Para fins de teste local, iremos criar um RBAC super permissivo, mas para ambientes produtivos precisamos refinar o RBAC:

```bash
cat <<EOF | kubectl apply -f -
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-all
  namespace: kubeflow-user-example-com
spec:
 rules:
 - {}
EOF
```

Execute o comando acima para aplicar a política de autorização diretamente no Kubernetes.

```bash
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: seaweedfs-s3
  namespace: kubeflow
spec:
  selector:
    app: seaweedfs
  ports:
    - protocol: TCP
      port: 8333
      targetPort: 8333
  type: ClusterIP
EOF
```

Agora é só testar o request para o endpoint:

```python
import requests

isvc_resp = KServe.get(name, namespace=namespace)
isvc_url = f"{isvc_resp['status']['address']['url']}/v1/models/sklearn-produtos:predict"

print(isvc_resp)

inference_input = {"instances": ["Figura Transformers Prime War Deluxe - E9687 - Hasbro", "Senhor dos aneis", "Senhor dos anéis"]}

response = requests.post(isvc_url, json=inference_input)
print(response.text)
```

Nesta seção, conseguimos treinar e efetuar o deploy do modelo via Notebook Jupyter de forma fácil, usando somente componentes do Kubeflow.
