0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GPUコンテナの高火力DOKでJanus(画像生成)を試す

Last updated at Posted at 2025-07-08

高火力 DOKはコンテナー型のGPUサービスで、NVIDIA V100やH100を実行時間課金で利用できるサービスです。

今回はこの高火力 DOKを使って、deepseek-ai/Janusを試してみました。

注意

JanusはDeepSeekが開発した画像生成AIです。人物やアニメ風画像生成などに特化したモデルとなっています。プロンプトベースで画像生成ができます。

参考

deepseek-ai/Janus: Janus-Series: Unified Multimodal Understanding and Generation Modelsにあるコードをベースに進めます。

とりあえず試す

コンテナレジストリにプッシュ済みのイメージを使って実行してみたい方は、高火力 DOKにて新しいタスクを作成し、以下の情報を入力してください。

項目 設定
イメージ dok-handson.sakuracr.jp/janus
環境変数 PROMPT = A stunning boy from kabul in red, white traditional clothing, blue eyes, brown hair

PROMPTは生成する画像の指示内容です。この他、以下のプロパティが指定できます。

項目 説明 デフォルト
IMAGE_SIZE 画像のサイズを指定します 384
IMAGE_NUMBER 生成する画像の数を指定します 10
S3_BUCKET オブジェクトストレージのバケット名を指定します
S3_ENDPOINT オブジェクトストレージのエンドポイントを指定します
S3_SECRET オブジェクトストレージのシークレットアクセスキーを指定します
S3_TOKEN オブジェクトストレージのアクセスキーIDを指定します

S3_で始まる環境変数は、さくらのオブジェクトストレージに画像を保存するためのものです。保存しない場合には指定しなくても問題ありません(Amazon S3なども利用できます)。さくらのオブジェクトストレージを利用する場合には、オブジェクトストレージ サービス基本情報 | さくらのクラウド マニュアルを参照してください。

スクリーンショット 2025-05-14 17.34.53.png

コンテナイメージの作成と登録

上記タスクで利用したDockerイメージを作成する手順は以下の通りです。完成版はgoofmint/dok-janusにありますので、実装時の参考にしてください。

Dockerfile の作成

deepseek-ai/Janus: Janus-Series: Unified Multimodal Understanding and Generation Modelsの内容に沿って、Dockerfileを作成します。

ベースイメージ

ベースは FROM pytorch/pytorch:2.2.1-cuda11.8-cudnn8-devel です。

FROM pytorch/pytorch:2.2.1-cuda11.8-cudnn8-devel

ライブラリのインストール

Pythonと、必要なライブラリをインストールします。

# 必要パッケージのインストール
RUN apt-get update && \
    apt-get install -y \
        libcupti-dev \
        git \
	wget \
        python3 \
        python3-pip && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 作業ディレクトリ
RUN mkdir /app /opt/artifact
WORKDIR /app

# Janus クローン
RUN git clone https://github.com/deepseek-ai/Janus.git .
RUN pip install -r requirements.txt --no-deps

そして、別途必要なライブラリをインストールします。PyTorchやFlashAttentionなどをインストールします。

RUN pip install --no-deps \
      boto3 \
      botocore \
      argparse \
      regex \
      python-dateutil \
      jmespath \
      safetensors>=0.4.3 \
      tokenizers \
      huggingface_hub
RUN pip install --index-url https://download.pytorch.org/whl/cu118 torch==2.2.1
RUN wget https://github.com/Dao-AILab/flash-attention/releases/download/v2.6.0.post1/flash_attn-2.6.0.post1+cu118torch2.2cxx11abiFALSE-cp310-cp310-linux_x86_64.whl
RUN pip install flash_attn-2.6.0.post1+cu118torch2.2cxx11abiFALSE-cp310-cp310-linux_x86_64.whl

後は 後述する runner.py と、 docker-entrypoint.sh をコピーします。

# スクリプトをコピー
COPY runner.py /app/runner.py
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

# 起動
CMD ["/bin/bash", "/docker-entrypoint.sh"]

Dockerfile全体

Dockerfileの全体は以下の通りです。

FROM pytorch/pytorch:2.2.1-cuda11.8-cudnn8-devel

# 必要パッケージのインストール
RUN apt-get update && \
    apt-get install -y \
        libcupti-dev \
        git \
	wget \
        python3 \
        python3-pip && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 作業ディレクトリ
RUN mkdir /app /opt/artifact
WORKDIR /app

# Janus クローン
RUN git clone https://github.com/deepseek-ai/Janus.git .
RUN pip install -r requirements.txt --no-deps

RUN pip install --no-deps \
      boto3 \
      botocore \
      argparse \
      regex \
      python-dateutil \
      jmespath \
      safetensors>=0.4.3 \
      tokenizers \
      huggingface_hub
RUN pip install --index-url https://download.pytorch.org/whl/cu118 torch==2.2.1
RUN wget https://github.com/Dao-AILab/flash-attention/releases/download/v2.6.0.post1/flash_attn-2.6.0.post1+cu118torch2.2cxx11abiFALSE-cp310-cp310-linux_x86_64.whl
RUN pip install flash_attn-2.6.0.post1+cu118torch2.2cxx11abiFALSE-cp310-cp310-linux_x86_64.whl
# スクリプトをコピー
COPY runner.py /app/runner.py
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

# 起動
CMD ["/bin/bash", "/docker-entrypoint.sh"]

docker-entrypoint.sh の作成

docker-entrypoint.sh はDockerコンテナー起動時に実行するスクリプトです。ここでは環境変数をチェックして、 runner.py を呼び出します。以下の内容で作成します。PROMTは必須で、他は任意です。SAKURA_ ではじまる環境変数は、高火力 DOK実行時に自動的に渡される変数になります。

#!/bin/bash
export TZ=${TZ:-Asia/Tokyo}

# 必須環境変数のチェック
missing=0

check_required() {
  if [ -z "${!1}" ]; then
    echo "Error: $1 is required but not set."
    missing=1
  fi
}

check_required PROMPT
check_required SAKURA_ARTIFACT_DIR
check_required SAKURA_TASK_ID

if [ "$missing" -eq 1 ]; then
  echo "Please set all required environment variables."
  exit 1
fi

# runner.py を引数付きで実行
pushd /app
  python3 runner.py \
    --prompt "$PROMPT" \
    --output "$SAKURA_ARTIFACT_DIR" \
    --id "$SAKURA_TASK_ID" \
    --img_size "${IMAGE_SIZE:-384}" \
    --img_num "${IMAGE_NUMBER:-10}" \
    ${S3_BUCKET:+--s3-bucket "$S3_BUCKET"} \
    ${S3_ENDPOINT:+--s3-endpoint "$S3_ENDPOINT"} \
    ${S3_SECRET:+--s3-secret "$S3_SECRET"} \
    ${S3_TOKEN:+--s3-token "$S3_TOKEN"}
popd

runner.py の作成

runner.py は実際に処理を行うスクリプトです。まず必要なライブラリをインポートします。

import os
import argparse
import PIL.Image
import torch
import numpy as np
import boto3
from transformers import AutoModelForCausalLM
from janus.models import MultiModalityCausalLM, VLChatProcessor

パラメータの取得

docker-entrypoint.sh から渡されたパラメータを取得します。

# 引数のパース
parser = argparse.ArgumentParser()
parser.add_argument('--output', type=str, required=True, help='出力先ディレクトリ')
parser.add_argument('--id', type=str, required=True, help='出力ファイル名のID。プリフィックス')
parser.add_argument('--img_size', type=int, default=384, help='出力画像サイズ(1辺)')
parser.add_argument('--img_num', type=int, default=10, help='出力する画像枚数')
parser.add_argument('--prompt', type=str, required=True, help='画像生成プロンプト')
parser.add_argument('--s3-bucket', help='S3のバケットを指定します。')
parser.add_argument('--s3-endpoint', help='S3互換エンドポイントのURLを指定します。')
parser.add_argument('--s3-secret', help='S3のシークレットアクセスキーを指定します。')
parser.add_argument('--s3-token', help='S3のアクセスキーIDを指定します。')

args = parser.parse_args()

オブジェクトストレージ用のオブジェクトを準備

S3_ではじまる環境変数があれば、それを使ってS3オブジェクトを作成します。

s3 = None
if args.s3_token and args.s3_secret and args.s3_bucket:
    # S3クライアントの作成
    s3 = boto3.client(
        's3',
        endpoint_url=args.s3_endpoint if args.s3_endpoint else None,
        aws_access_key_id=args.s3_token,
        aws_secret_access_key=args.s3_secret)

モデルの準備

今回は Janus-1.3B を利用しています。この他、 Janus-Pro-1BJanus-Pro-7B などが用意されています。

# モデルの準備
model_path = "deepseek-ai/Janus-1.3B"
vl_chat_processor: VLChatProcessor = VLChatProcessor.from_pretrained(model_path)
tokenizer = vl_chat_processor.tokenizer
vl_gpt: MultiModalityCausalLM = AutoModelForCausalLM.from_pretrained(
    model_path, trust_remote_code=True
)
vl_gpt = vl_gpt.to(torch.bfloat16).cuda().eval()

プロンプトの整形

入力されたプロンプトに基づいて、実行するためのプロンプトを作成します。

# プロンプト整形
conversation = [
    {"role": "User", "content": args.prompt},
    {"role": "Assistant", "content": ""},
]
sft_format = vl_chat_processor.apply_sft_template_for_multi_turn_prompts(
    conversations=conversation,
    sft_format=vl_chat_processor.sft_format,
    system_prompt="",
)
prompt = sft_format + vl_chat_processor.image_start_tag

画像生成

画像生成を行う generate 関数を作成します。この関数ではJanusを使って画像生成後、結果を指定したディレクトリへの保存と、必要があればオブジェクトストレージへ保存します。

コード自体はdeepseek-ai/Janus: Janus-Series: Unified Multimodal Understanding and Generation Modelsをカスタマイズしたものです。

@torch.inference_mode()
def generate(
    mmgpt: MultiModalityCausalLM,
    vl_chat_processor: VLChatProcessor,
    prompt: str,
    output_dir: str,
    prefix: str,
    img_size: int = 384,
    img_num: int = 10,
    temperature: float = 1,
    cfg_weight: float = 5,
    image_token_num_per_image: int = 576,
    patch_size: int = 16,
):
    input_ids = vl_chat_processor.tokenizer.encode(prompt)
    input_ids = torch.LongTensor(input_ids)

    tokens = torch.zeros((img_num * 2, len(input_ids)), dtype=torch.int).cuda()
    for i in range(img_num * 2):
        tokens[i, :] = input_ids
        if i % 2 != 0:
            tokens[i, 1:-1] = vl_chat_processor.pad_id

    inputs_embeds = mmgpt.language_model.get_input_embeddings()(tokens)

    generated_tokens = torch.zeros((img_num, image_token_num_per_image), dtype=torch.int).cuda()

    for i in range(image_token_num_per_image):
        outputs = mmgpt.language_model.model(
            inputs_embeds=inputs_embeds,
            use_cache=True,
            past_key_values=outputs.past_key_values if i != 0 else None
        )
        hidden_states = outputs.last_hidden_state

        logits = mmgpt.gen_head(hidden_states[:, -1, :])
        logit_cond = logits[0::2, :]
        logit_uncond = logits[1::2, :]

        logits = logit_uncond + cfg_weight * (logit_cond - logit_uncond)
        probs = torch.softmax(logits / temperature, dim=-1)

        next_token = torch.multinomial(probs, num_samples=1)
        generated_tokens[:, i] = next_token.squeeze(dim=-1)

        next_token = torch.cat([next_token.unsqueeze(dim=1)] * 2, dim=1).view(-1)
        img_embeds = mmgpt.prepare_gen_img_embeds(next_token)
        inputs_embeds = img_embeds.unsqueeze(dim=1)

    dec = mmgpt.gen_vision_model.decode_code(
        generated_tokens.to(dtype=torch.int),
        shape=[img_num, 8, img_size // patch_size, img_size // patch_size]
    )
    dec = dec.to(torch.float32).cpu().numpy().transpose(0, 2, 3, 1)
    dec = np.clip((dec + 1) / 2 * 255, 0, 255).astype(np.uint8)

    os.makedirs(output_dir, exist_ok=True)
    for i in range(img_num):
        save_path = os.path.join(output_dir, f"{prefix}_{i}.jpg")
        PIL.Image.fromarray(dec[i]).save(save_path)
        if s3 is not None:
            s3.upload_file(
                Filename=save_path,
                Bucket=args.s3_bucket,
                Key=os.path.basename(save_path))

関数を実行

最後に、 generate 関数を実行します。

generate(
    vl_gpt,
    vl_chat_processor,
    prompt,
    output_dir=args.output,
    prefix=args.id,
    img_size=args.img_size,
    img_num=args.img_num,
)

全体の処理

runner.py の全体の処理は以下の通りです。

import os
import argparse
import PIL.Image
import torch
import numpy as np
import boto3
from transformers import AutoModelForCausalLM
from janus.models import MultiModalityCausalLM, VLChatProcessor

# 引数のパース
parser = argparse.ArgumentParser()
parser.add_argument('--output', type=str, required=True, help='出力先ディレクトリ')
parser.add_argument('--id', type=str, required=True, help='出力ファイル名のID。プリフィックス')
parser.add_argument('--img_size', type=int, default=384, help='出力画像サイズ(1辺)')
parser.add_argument('--img_num', type=int, default=10, help='出力する画像枚数')
parser.add_argument('--prompt', type=str, required=True, help='画像生成プロンプト')
parser.add_argument('--s3-bucket', help='S3のバケットを指定します。')
parser.add_argument('--s3-endpoint', help='S3互換エンドポイントのURLを指定します。')
parser.add_argument('--s3-secret', help='S3のシークレットアクセスキーを指定します。')
parser.add_argument('--s3-token', help='S3のアクセスキーIDを指定します。')

args = parser.parse_args()

s3 = None
if args.s3_token and args.s3_secret and args.s3_bucket:
    # S3クライアントの作成
    s3 = boto3.client(
        's3',
        endpoint_url=args.s3_endpoint if args.s3_endpoint else None,
        aws_access_key_id=args.s3_token,
        aws_secret_access_key=args.s3_secret)

# モデルの準備
model_path = "deepseek-ai/Janus-1.3B"
vl_chat_processor: VLChatProcessor = VLChatProcessor.from_pretrained(model_path)
tokenizer = vl_chat_processor.tokenizer
vl_gpt: MultiModalityCausalLM = AutoModelForCausalLM.from_pretrained(
    model_path, trust_remote_code=True
)
vl_gpt = vl_gpt.to(torch.bfloat16).cuda().eval()

# プロンプト整形
conversation = [
    {"role": "User", "content": args.prompt},
    {"role": "Assistant", "content": ""},
]
sft_format = vl_chat_processor.apply_sft_template_for_multi_turn_prompts(
    conversations=conversation,
    sft_format=vl_chat_processor.sft_format,
    system_prompt="",
)
prompt = sft_format + vl_chat_processor.image_start_tag

@torch.inference_mode()
def generate(
    mmgpt: MultiModalityCausalLM,
    vl_chat_processor: VLChatProcessor,
    prompt: str,
    output_dir: str,
    prefix: str,
    img_size: int = 384,
    img_num: int = 10,
    temperature: float = 1,
    cfg_weight: float = 5,
    image_token_num_per_image: int = 576,
    patch_size: int = 16,
):
    input_ids = vl_chat_processor.tokenizer.encode(prompt)
    input_ids = torch.LongTensor(input_ids)

    tokens = torch.zeros((img_num * 2, len(input_ids)), dtype=torch.int).cuda()
    for i in range(img_num * 2):
        tokens[i, :] = input_ids
        if i % 2 != 0:
            tokens[i, 1:-1] = vl_chat_processor.pad_id

    inputs_embeds = mmgpt.language_model.get_input_embeddings()(tokens)

    generated_tokens = torch.zeros((img_num, image_token_num_per_image), dtype=torch.int).cuda()

    for i in range(image_token_num_per_image):
        outputs = mmgpt.language_model.model(
            inputs_embeds=inputs_embeds,
            use_cache=True,
            past_key_values=outputs.past_key_values if i != 0 else None
        )
        hidden_states = outputs.last_hidden_state

        logits = mmgpt.gen_head(hidden_states[:, -1, :])
        logit_cond = logits[0::2, :]
        logit_uncond = logits[1::2, :]

        logits = logit_uncond + cfg_weight * (logit_cond - logit_uncond)
        probs = torch.softmax(logits / temperature, dim=-1)

        next_token = torch.multinomial(probs, num_samples=1)
        generated_tokens[:, i] = next_token.squeeze(dim=-1)

        next_token = torch.cat([next_token.unsqueeze(dim=1)] * 2, dim=1).view(-1)
        img_embeds = mmgpt.prepare_gen_img_embeds(next_token)
        inputs_embeds = img_embeds.unsqueeze(dim=1)

    dec = mmgpt.gen_vision_model.decode_code(
        generated_tokens.to(dtype=torch.int),
        shape=[img_num, 8, img_size // patch_size, img_size // patch_size]
    )
    dec = dec.to(torch.float32).cpu().numpy().transpose(0, 2, 3, 1)
    dec = np.clip((dec + 1) / 2 * 255, 0, 255).astype(np.uint8)

    os.makedirs(output_dir, exist_ok=True)
    for i in range(img_num):
        save_path = os.path.join(output_dir, f"{prefix}_{i}.jpg")
        PIL.Image.fromarray(dec[i]).save(save_path)
        if s3 is not None:
            s3.upload_file(
                Filename=save_path,
                Bucket=args.s3_bucket,
                Key=os.path.basename(save_path))

# 画像生成
generate(
    vl_gpt,
    vl_chat_processor,
    prompt,
    output_dir=args.output,
    prefix=args.id,
    img_size=args.img_size,
    img_num=args.img_num,
)

Dockerイメージのビルド

上記の内容で、Dockerイメージをビルドします。Linux環境などで行います。

コンテナレジストリの用意

Dockerイメージを登録するコンテナレジストリを作成します。さくらのクラウドではLAB機能で、コンテナレジストリを提供しています。さくらのクラウドにログインしたら さくらのクラウド を選択します。

image.png

左側のメニューの グローバル の中にある コンテナレジストリ を選択します。

image.png

追加 を押して、コンテナレジストリを作成します。最低限、以下の入力が必要です。

項目 設定
名前 分かりやすい、任意の名前を入力してください
コンテナレジストリ名 ドメイン名に使われます。以下では、 EXAMPLE.sakuracr.jp として説明します
公開設定 Pullのみとします

ユーザーの作成

コンテナレジストリを作成したら、作成したコンテナレジストリを一覧でダブルクリックします。

image.png

詳細表示にて、ユーザータブをクリックします。

image.png

追加ボタンを押し、ユーザーを作成します。 YOUR_USER_NAMEPASSWORD は任意のものを指定してください。

項目 設定
ユーザー名 YOUR_USER_NAME
パスワード YOUR_PASSWORD
ユーザ権限設定 All

Dockerイメージのビルド

DockerイメージはLinuxで行います。今回はUbuntu 24.04を使っています。Dockerが使える環境であれば、Windows + WSL2でも問題ありません。macOSの場合、アーキテクチャが異なるので動かせないかも知れません(未検証です)。

EXAMPLE.sakuracr.jp の部分は、作成したコンテナレジストリのドメイン名に置き換えてください。また、 janus は任意の名前で大丈夫です(以下はその名称で読み替えてください)。

sudo docker build -t EXAMPLE.sakuracr.jp/janus:latest .

コンテナレジストリへのログイン

作成したコンテナレジストリにログインします。ログインIDとパスワードが求められるので、作成したものを入力してください。

sudo docker login EXAMPLE.sakuracr.jp

イメージのプッシュ

作成したイメージをコンテナレジストリにプッシュします。イメージサイズが大きいので、数十分かかります。

sudo docker push EXAMPLE.sakuracr.jp/janus:latest

タスクを作成する

後は最初と同じように高火力 DOKでタスクを作成、実行します。

項目 設定
イメージ dok-handson.sakuracr.jp/janus
環境変数 PROMPT = A stunning boy from kabul in red, white traditional clothing, blue eyes, brown hair
IMAGE_NUM = 20

FireShot Capture 393 - 高火力 DOK - タスク › 新規作成 - secure.sakura.ad.jp.jpg

実行が完了し、画像が生成できていれば成功です。

まとめ

今回はJanusを使って、高火力 DOK上で画像生成を行いました。まずは実行できるのみ、次にDockerイメージの作成と段階的に進められるようにしています。画像生成のように、処理に時間がかかるものを利用する際に高火力 DOKは便利です。

高火力 DOKはタスクを多数立ち上げて、後は結果を待つのみと言った使い方ができます。ぜひAI・機械学習に活用してください。

コンテナー型GPUクラウドサービス 高火力 DOK(ドック) | さくらインターネット

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?