3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Azure OpenAI】ChatGPTのレイテンシをどうにかしたい

Posted at

ChatGPT呼び出しの速度が安定しない

どうにもこうにもレイテンシが安定しないです。LLMの特徴なのか複雑な指示や長文回答を要求すると遅くなるみたいですし、それだけでなく基盤が繁忙なのか突然リクエストが遅くなったり500エラーが返ったりすることもありますよね。
(ただ安定するときはとても安定しているので、もっと安定すれば今回の検証はそもそも不要かもです。というか、不要になったらいいなぁ。)

なお、Azure OpenAI ServiceはSLAはあるものの、レイテンシに関するSLAはありません。ぬぐぐ。

なので、アプリに組み込むのであればタイムアウト&リトライが必須ですよね。でもそういえばAzure OpenAIはマルチリージョンでリソースが作れるので、それで何とかできないなか?という気持ちでマルチリージョン並列呼び出しを試してみました。

コストが掛け算で増えていきますけど、もともとが(gpt-35-turboなら)低額なので、許容できるのであれば検討には値するかなと思います。
また、以下のコードには例外ハンドリングもタイムアウトリトライもいれていないとりあえず動作のみの検証になります。

マルチリージョン並列呼び出し

ようは、東アメリカと南西アメリカ、フランス、イギリス、日本で同じリクエストを並列で実行し、最初に回答が返ってきたものを使う。これだけです。

以下のようなリージョン単位でのChatGPT呼び出し関数を定義します。Pythonです。なお、Pythonのopenaiのライブラリですが、openai.api_key="XXXX"のあたりからスレッドセーフ感を感じません。.NETのライブラリはスレッドセーフみたいなんですが、pythonライブラリがそうでなさそう(きちんと調べていないです)ので、HTTPのリクエストを直接呼び出す方式でやりました。

以下のように並列呼び出ししたいリソース単位でメソッドを定義します。このコードだとエンドポイントAOAI_ENDPOINT_1キーAOAI_API_KEY_1モデル名AOAI_MODEL_1を使っています。同じように_2_3で各リージョンのインスタンスを指定します。コルーチンしたいので、aiohttpをつかって、POSTリクエストを処理します。

import aiohttp

async def call_chatgpt_at_xxx (messages,temperature=0):
    url = f"{settings.AOAI_ENDPOINT_1}openai/deployments/{settings.AOAI_MODEL_1}/chat/completions?api-version=2023-05-15"
    headers = {
    "Content-Type": "application/json",
    "api-key": settings.AOAI_API_KEY_1
    }
    data = {
    "messages": messages,
    "temperature": temperature,
    }

    async with aiohttp.ClientSession() as session:
        response = await session.post(url, headers=headers, json=data)
        response = await response.json()

    return response['choices'][0]['message']['content']

できたChatGPT呼び出し関数をタスクとして定義します。とりあえずテスト用のプロンプトをいれます。その上でタスクを同時実行し、最初の回答が返ってきたら残りをキャンセルして、最初の回答を返す感じです。以下のコードだと、東アメリカと東日本の2リージョンでの並列実行です。。

import asyncio
・・()・・

    messages =  [  
    {'role':'system', 'content':'あなたはAIアシスタントです'},
    {'role':'user', 'content':'今日の格言を教えてください'},
    ]

    tasks = [
        asyncio.create_task(call_chatgpt_japan(messages),name="EAST JAPAN RESOURCE!"),
        asyncio.create_task(call_chatgpt_eastus(messages),name="EAST US RESOURCE!"),
    ]

    # 一番最初に完了したタスクだけを取得する
    done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)

    # 未完了のタスクはキャンセルする
    for task in pending:
        task.cancel()

    # 完了したタスクから結果を取得する
    for task in done:
        first_response = task.result()
        first_name = task.get_name()
        return first_response,first_name

実装はこれだけですが、動作確認のため、ログを出して動かしてみた結果は以下となりました。

2リージョン並列呼び出しの結果

1回目
EAST JAPAN RESOURCE!
「成功は、行動を起こす者にのみ訪れる」
0.48秒

EAST US RESOURCE!
「成功は、行動を起こす者にのみ訪れる」
1.30秒
2回目
EAST JAPAN RESOURCE!
「成功は、行動を起こす者にのみ訪れる」
0.30秒

EAST US RESOURCE!
「成功は、行動を起こす者にのみ訪れる」
1.61秒

タイミングがよかったのか、いずれもそんなにレイテンシ遅くなく返ってきてますが、2リージョン並列呼び出しにすることで、1回目で約0.8秒、2回目で約1秒短縮できそうですね。いや、たまにもっと遅くなったりするんですよ。なので、レスポンスが重要ならマルチリージョンで呼びだして最初の回答を使うのはありかもですね。

まとめ

Azure OpenAI のChatGPT呼び出しをマルチリージョン並列呼び出しにすることで、たまたまどこかのリージョンがリクエスト集中でパツンパツンになっていても影響の少ないリージョンの低レイテンシの回答を使うことができそうです。

ただやってみて思いましたが、

  • 利用するライブラリがスレッドセーフでない場合は、自前で呼びだし関数を作成しないとならない
  • 自前の関数に、自前のタイムアウトも設定しないとならない
  • 例外ハンドリングがめんどくさそう、特に全リージョンタイムアウトするようなケースなども考えないとならない
  • コストが並列した数の掛け算になる。
    あたりが実際にやる場合には、検討/考慮ポイントですね。。

やっぱり基本に返って、

  • タイムアウトの設計とリトライでカバー
  • ストリームを使うことでユーザビリティをカバー
  • UI/UXでカバー

でできるならそっちの方が良さそうです。が、1つの検討材料として見てもらえたらなと思います。

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?