Rocky LinuxとRadeon RX 480でROCmを使ってローカルLLMを動かしてみる(GUI編)

前回、llama.cppを使ってLLMをローカルで動かすことができたので、サーバ機能を使ってAPIサーバを起動し、ブラウザから使ってみようと思います。

llama.cppのserverを使って、APIサーバを起動します。

# /work/llama.cpp/server -ngl 39 -m /work/model/ELYZA-japanese-Llama-2-7b-instruct-q4_K_M.gguf --host 0.0.0.0 --port 8080

curlでAPIにPOSTして動作確認します。

# curl --request POST\
     --url http://localhost:8080/completion\
     --header "Content-Type: application/json"\
     --data '{"prompt": "[INST] <<SYS>>あなたは誠実で優秀な日本人のアシスタントです。<</SYS>>Pythonでmysqlか らselectするサンプルを実装して[/INST]","n_predict": 256}'

{"content":" PythonでMySQLからのデータ選択を行うためのサンプルコードを作成します。\n\nまず、PythonでMySQLに接続するモジュールを求めます。ここではPyMySQLモジュールを使用します。\n```\nimport pymysql\n```\n次に、MySQLのサーバー情報、ユーザー 名、パスワード、データベース名を指定します。\n```\nserver = \"localhost\"\nuser = \"your_username\"\npassword = \"your_password\"\ndb = \"your_database\"\n```\n次に、PyMySQLのConnect関数を呼び出し、接続情報を渡してMySQLに接続します。\n```\nconn = pymysql.connect(host=server, user=user, passwd=password, db=db)\n```\n次 に、PyMySQLのCurserを開き、SQL文を指定します。\n```\ncursor = conn.cursor()\nquery = \"SELECT * FROM your_table\"\ncursor.execute(query)\n```\n次に、PyMySQLのfetchall()関数を呼び出し、データセレクトの結果をデータベースの行として取得します。\n```\nresult = cursor.fetchall()\n```\n次に、PyMySQLのcommit()関数を呼び出し、 データベースにコミット","id_slot":0,"stop":true,"model":"/work/model/ELYZA-japanese-Llama-2-13b-fast-instruct-q4_K_M.gguf","tokens_predicted":256,"tokens_evaluated":37,"generation_settings":{"n_ctx":512,"n_predict":-1,"model":"/work/model/ELYZA-japanese-Llama-2-13b-fast-instruct-q4_K_M.gguf","seed":4294967295,"temperature":0.800000011920929,"dynatemp_range":0.0,"dynatemp_exponent":1.0,"top_k":40,"top_p":0.949999988079071,"min_p":0.05000000074505806,"tfs_z":1.0,"typical_p":1.0,"repeat_last_n":64,"repeat_penalty":1.0,"presence_penalty":0.0,"frequency_penalty":0.0,"penalty_prompt_tokens":[],"use_penalty_prompt_tokens":false,"mirostat":0,"mirostat_tau":5.0,"mirostat_eta":0.10000000149011612,"penalize_nl":false,"stop":[],"n_keep":0,"n_discard":0,"ignore_eos":false,"stream":false,"logit_bias":[],"n_probs":0,"min_keep":0,"grammar":"","samplers":["top_k","tfs_z","typical_p","top_p","min_p","temperature"]},"prompt":"[INST] <<SYS>>あなたは誠実で優秀な日本 人のアシスタントです。<</SYS>>Pythonでmysqlからselectするサンプルを実装して[/INST]","truncated":false,"stopped_eos":false,"stopped_word":false,"stopped_limit":true,"stopping_word":"","tokens_cached":292,"timings":{"prompt_n":37,"prompt_ms":31625.747,"prompt_per_token_ms":854.7499189189189,"prompt_per_second":1.1699328398472295,"predicted_n":256,"predicted_ms":22570.624,"predicted_per_token_ms":88.1665,"predicted_per_second":11.342176450239036}}redicted_per_token_ms":83.9495546875,"predicted_per_second":11.911915479748208}}

WebAPIでも同様に応答が返ってきますね。

WebAPIとお話しするのにcrulを使っていてはコマンドライン実行と変わらないのでWebサーバを立てます。
結構いろんなUIがあるみたいですが、とりあえずシンプルに行きます。

# dnf install -y httpd

UIの部分です。/var/www/html/index.htmlとして保存します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="./script.js" defer></script>
    <title>Chat with llama.cpp</title>
    <style>
        body { 
            font-family: Arial, sans-serif; 
            background-color: #f4f4f9; 
            margin: 0; 
            padding: 0; 
            display: flex; 
            justify-content: center; 
            align-items: center; 
            height: 100vh; 
        }
        .chat-container { 
            width: 80%; 
            max-width: 1200px; 
            background-color: #fff; 
            margin: auto; 
            padding: 20px; 
            border: 1px solid #ddd; 
            border-radius: 10px; 
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 
            display: flex; 
            flex-direction: column; 
            height: calc(100vh - 40px); 
        }
        .chat-box { 
            flex: 1; 
            width: 100%; 
            border: 1px solid #ddd; 
            padding: 10px; 
            overflow-y: scroll; 
            border-radius: 5px; 
            background-color: #fafafa; 
            margin-bottom: 10px; 
        }
        .input-box-container { 
            display: flex; 
        }
        .input-box { 
            flex: 1; 
            padding: 10px; 
            border: 1px solid #ddd; 
            border-radius: 5px; 
            margin-right: 10px; 
            resize: none; 
            height: 100px; 
        }
        .send-button { 
            padding: 10px 20px; 
            border: none; 
            background-color: #4CAF50; 
            color: white; 
            border-radius: 5px; 
            cursor: pointer; 
            transition: background-color 0.3s; 
        }
        .send-button:disabled {
            background-color: #9E9E9E;
            cursor: not-allowed;
        }
        .send-button:hover:enabled { 
            background-color: #45a049; 
        }
        .message { 
            margin: 5px 0; 
            padding: 10px; 
            border-radius: 5px; 
        }
        .user { 
            background-color: #d1e7dd; 
            align-self: flex-end; 
            text-align: right; 
        }
        .bot { 
            background-color: #f8d7da; 
            align-self: flex-start; 
            text-align: left; 
        }
    </style>
</head>
<body>
    <div class="chat-container">
        <h1>Chat with llama.cpp</h1>
        <div class="chat-box" id="chat-box"></div>
        <div class="input-box-container">
            <textarea id="user-input" class="input-box" placeholder="Type a message..."></textarea>
            <button id="send-button" class="send-button" onclick="sendMessage()">Send</button>
        </div>
    </div>
</body>
</html>

APIとやり取りするscript.jsです。

let botThinking = false;
let botToking = false;

function formatCodeBlock(text) {
    const codeRegex = /```([\s\S]+?)```/g;
    return text.replace(codeRegex, (match, p1) => {
        const codeElement = document.createElement('pre');
        const codeContent = document.createElement('code');
        codeContent.textContent = p1.trim();
        codeElement.appendChild(codeContent);
        return codeElement.outerHTML;
    });
}

function formatMessageText(text) {
    return text.replace(/\n/g, '<br>');
}

function updateSendButtonState() {
    const sendButton = document.getElementById('send-button');
    sendButton.disabled = botThinking || botToking;
}

async function sendMessage() {
    const userInput = document.getElementById('user-input').value;
    if (!userInput) return;

    const chatBox = document.getElementById('chat-box');
    appendMessage('You: ' + formatMessageText(userInput), 'user');

    const prompt = `[INST] <<SYS>>あなたは誠実で優秀な日本人のアシスタントです。<</SYS> ${userInput}[/INST]`;

    const botMessage = appendMessage('Bot: ・・・', 'bot');

    botThinking = true;
    updateSendButtonState();

    try {
        const response = await fetch('http://127.0.0.1:8080/completions', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer YOUR_API_KEY'
            },
            body: JSON.stringify({
                prompt: prompt,
                max_tokens: 256,
                stream: true
            })
        });

        const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();

        while (true) {
            const { value, done } = await reader.read();
            if (done) break;

            const lines = value.split('\n').filter(line => line.trim() !== '');
            for (const line of lines) {
                if (line.startsWith('data:')) {
                    const json = line.substring(5).trim();
                    if (json !== '[DONE]') {
                        try {
                            const parsed = JSON.parse(json);
                            if (parsed.content) {
                                if (botThinking) {
                                    botMessage.innerHTML = 'Bot: ';
                                    botThinking = false;
                                    botToking = true;
                                    updateSendButtonState();
                                }

                                botMessage.innerHTML += formatCodeBlock(formatMessageText(parsed.content));
                                chatBox.scrollTop = chatBox.scrollHeight;
                            }
                            if (parsed.stop || parsed.content == "") {
                                reader.cancel();
                                botToking = false;
                                updateSendButtonState();
                                break;
                            }
                        } catch (e) {
                            console.error('Failed to parse JSON:', e);
                        }
                    }
                }
            }
        }
    } catch (error) {
        botMessage.innerHTML = 'Bot: エラーが発生しました。';
        console.error('Error:', error);
    }

    document.getElementById('user-input').value = '';
    chatBox.scrollTop = chatBox.scrollHeight;
}

function appendMessage(text, type) {
    const chatBox = document.getElementById('chat-box');
    const message = document.createElement('div');
    message.classList.add('message', type);
    message.innerHTML = text;
    chatBox.appendChild(message);
    chatBox.scrollTop = chatBox.scrollHeight;
    return message;
}

document.getElementById('user-input').addEventListener('keydown', function(event) {
    if (event.key === 'Enter' && !event.shiftKey) {
        event.preventDefault();
        sendMessage();
    }
});

ファイルを置いたらhttpdを起動します。

# systemctl enable --now httpd

ブラウザで開くと以下のようになります。

過去の会話を考慮したりはしていないので、会話という感じではないですが、コマンドを打つより使い勝手良いはずです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)