k3sでollamaを動かす

k3s
ollama
kubernetes
AI

2024-09-15

#目次

#はじめに

今回は、k3sでollamaを動かしてみます。
前回、k3sでNvidia GPUを使用するを記載しましたが、今回はその環境を利用して、ollamaを動かしてみます。

#ollamaとは

Get up and running with large language models.
Run Llama 3.1, Phi 3, Mistral, Gemma 2, and other models. Customize and create your own.

出典:https://ollama.com/

様々なLLM(Large Language Model)を、あつかうことができるソフトウェアのようです。

ollamaのGithubのREADMEにはもう少し用途が書かれています。
Customize a model, CLI, REST APIなど。
モデルのカスタマイズについては今後調査していきたいと思いますが、REST APIを使って、アプリと連携できるようにしたいと思います。

#ollamaのhelm chartを作る

helm chartを作成する。

helm create ollama

templateが作成されるので、これをベースにして編集していきます。
ollamaのk8sのExampleがあるので、これと同じように設定していきます。
いくつか工夫している点があるので、それを記載します。

  • pvcを作成して、モデルを保存するディレクトリを永続化する

    これにより毎回再起動するたびにモデルを再度ダウンロードする必要がなくなります。

  • initContainersを追加して、モデルを事前にダウンロードする

    初回起動時は、モデルがダウンロードされていないため次のようなエラーとなります。

    ❯ curl http://localhost:11434/api/generate -d '{
      "model": "llama3.1",
      "prompt":"Why is the sky blue?"
      }'
      {"error":"model \"llama3.1\" not found, try pulling it first"}
    

    この記事では、7shi/tanuki-dpo-v1.0:8b-q6_Kという日本語に特化したモデルを使用します。
    このモデルは6GB程度あり、ダウンロードに時間がかかりますので、起動時に自動でダウンロードするようにしておくと便利です。
    deployments.yamlに直接モデル名を記載していますが、values.yamlで変更可能するのが望ましいです。

  • strategyをRecreateに設定する

    これは、GPUマシンが1台しかないので、RollingUpdateだと、GPUリソースが足りずにPodが起動できないためです。

helm chartの差分は、以下の通りです。

diff --git a/charts/ollama/templates/deployment.yaml b/charts/ollama/templates/deployment.yaml
index 5116529..4a17c0b 100644
--- a/charts/ollama/templates/deployment.yaml
+++ b/charts/ollama/templates/deployment.yaml
@@ -23,6 +23,7 @@ spec:
         {{- toYaml . | nindent 8 }}
         {{- end }}
     spec:
+      runtimeClassName: nvidia
       {{- with .Values.imagePullSecrets }}
       imagePullSecrets:
         {{- toYaml . | nindent 8 }}
@@ -30,16 +31,38 @@ spec:
       serviceAccountName: {{ include "ollama.serviceAccountName" . }}
       securityContext:
         {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      initContainers:
+        - name: init-ollama
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          command: ["sh", "-c"]
+          args:
+          - |
+            ollama serve &
+            ollama pull 7shi/tanuki-dpo-v1.0:8b-q6_K
+          {{- with .Values.volumeMounts }}
+          volumeMounts:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+
       containers:
         - name: {{ .Chart.Name }}
           securityContext:
             {{- toYaml .Values.securityContext | nindent 12 }}
+          strategy:
+            type: {{ .Values.strategy.type }}
           image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
           imagePullPolicy: {{ .Values.image.pullPolicy }}
           ports:
             - name: http
               containerPort: {{ .Values.service.port }}
               protocol: TCP
+          env:
+          - name: PATH
+            value: /usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+          - name: LD_LIBRARY_PATH
+            value: /usr/local/nvidia/lib:/usr/local/nvidia/lib64
+          - name: NVIDIA_DRIVER_CAPABILITIES
+            value: compute,utility
           livenessProbe:
             {{- toYaml .Values.livenessProbe | nindent 12 }}
           readinessProbe:
diff --git a/charts/ollama/values.yaml b/charts/ollama/values.yaml
index 0508d93..8f141b4 100644
--- a/charts/ollama/values.yaml
+++ b/charts/ollama/values.yaml
@@ -5,15 +5,18 @@
 replicaCount: 1

 image:
-  repository: nginx
+  repository: ollama/ollama
   pullPolicy: IfNotPresent
   # Overrides the image tag whose default is the chart appVersion.
-  tag: ""
+  tag: "latest"

 imagePullSecrets: []
 nameOverride: ""
 fullnameOverride: ""

+strategy:
+  type: Recreate
+
 serviceAccount:
   # Specifies whether a service account should be created
   create: true
@@ -41,7 +44,7 @@ securityContext: {}

 service:
   type: ClusterIP
-  port: 80
+  port: 11434

 ingress:
   enabled: false
@@ -59,17 +62,9 @@ ingress:
   #    hosts:
   #      - chart-example.local

-resources: {}
-  # We usually recommend not to specify default resources and to leave this as a conscious
-  # choice for the user. This also increases chances charts run on environments with little
-  # resources, such as Minikube. If you do want to specify resources, uncomment the following
-  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
-  # limits:
-  #   cpu: 100m
-  #   memory: 128Mi
-  # requests:
-  #   cpu: 100m
-  #   memory: 128Mi
+resources:
+  limits:
+    nvidia.com/gpu: 1

 livenessProbe:
   httpGet:
@@ -87,21 +82,28 @@ autoscaling:
   targetCPUUtilizationPercentage: 80
   # targetMemoryUtilizationPercentage: 80

+pvc:
+  name: ollama-pvc
+  storageClass: local-path
+  accessMode: ReadWriteOnce
+  size: 100Gi
+
 # Additional volumes on the output Deployment definition.
-volumes: []
-# - name: foo
-#   secret:
-#     secretName: mysecret
-#     optional: false
+volumeMounts:
+- name: ollama-volume
+  mountPath: /root/.ollama

 # Additional volumeMounts on the output Deployment definition.
-volumeMounts: []
-# - name: foo
-#   mountPath: "/etc/foo"
-#   readOnly: true
+volumes:
+- name: ollama-volume
+  persistentVolumeClaim:
+    claimName: ollama-pvc

 nodeSelector: {}

-tolerations: []
+tolerations:
+- key: nvidia.com/gpu
+  operator: Exists
+  effect: NoSchedule

 affinity: {}

#ollamaをデプロイする

作成したhelm chartをデプロイする。

helm install ollama ./charts/ollama

#動作確認

ポートフォーワードする。

kubectl port-forward svc/ollama -n ollama 11434:11434

curlでAPIを叩く。

❯ curl http://localhost:11434/api/generate -d '{
"model": "7shi/tanuki-dpo-v1.0:8b-q6_K",
"prompt": "こんにちは"
}'
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:41.181052705Z","response":"こんにちは","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:41.373976437Z","response":"!","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:41.437123752Z","response":"お","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:41.501613389Z","response":"元気","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:41.565656614Z","response":"ですか","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:41.756289163Z","response":"?","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:41.820054454Z","response":"今日","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:41.883317176Z","response":"はどんな","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:41.948509158Z","response":"ことを","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.014316521Z","response":"お話","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.078486035Z","response":"し","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.143537291Z","response":"しましょう","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.207232079Z","response":"か","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.40032952Z","response":"?","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.464766457Z","response":"何か","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.534158324Z","response":"質問","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.598727812Z","response":"や","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.663554856Z","response":"相談","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.727568585Z","response":"があれば","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.793013514Z","response":"、","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.857068501Z","response":"ぜひ","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.920863759Z","response":"教え","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:42.984698811Z","response":"てくださいね","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:43.048703974Z","response":"。","done":false}
{"model":"7shi/tanuki-dpo-v1.0:8b-q6_K","created_at":"2024-09-14T23:50:43.115757168Z","response":"","done":true,"done_reason":"stop","context":[1,272,1411,276,269,12311,273,680,530,297,3994,298,270,381,2023,273,950,1219,274,4074,354,1045,2850,273,1530,3314,270,26,8,26,8,48979,272,3994,327,26,8,3124,26,8,26,8,48979,272,1045,2850,327,26,8,3124,252,201,142,314,1785,1498,252,201,172,620,4785,785,1770,301,1889,316,252,201,172,806,1335,302,1185,2287,269,1905,930,294,346,365,12354,441,270],"total_duration":3912932817,"load_duration":1775721891,"prompt_eval_count":50,"prompt_eval_duration":200861000,"eval_count":31,"eval_duration":1934733000}

うまく動作しているようです。

runtimeClassName: nvidiaを指定しないとCPUで動作しますが、速度を比べても数倍早いと思います。
使っているGPUは、RTX 2060ですが、私にとっては、これで十分な速度が出ています。

#参考にした記事

#まとめ

今回は、k3sでollamaを動かしてみました。
この記事では、k3sで動作させていますが、k3sに限らず、GPUがプロビジョニングされているk8sクラスタであれば、同様に動作すると思います。
ローカルでLLMを動かすのは、利用制限やお金のことを考えずに、自由に使えるので、AIの学習や研究・実験には最適だと思います。