Add dual GPU support with web UI selector

Features:
- Built custom ROCm container for AMD RX 6800 GPU
- Added GPU selection toggle in web UI (NVIDIA/AMD)
- Unified model names across both GPUs for seamless switching
- Vision model always uses NVIDIA GPU (optimal performance)
- Text models (llama3.1, darkidol) can use either GPU
- Added /gpu-status and /gpu-select API endpoints
- Implemented GPU state persistence in memory/gpu_state.json

Technical details:
- Multi-stage Dockerfile.llamaswap-rocm with ROCm 6.2.4
- llama.cpp compiled with GGML_HIP=ON for gfx1030 (RX 6800)
- Proper GPU permissions without root (groups 187/989)
- AMD container on port 8091, NVIDIA on port 8090
- Updated bot/utils/llm.py with get_current_gpu_url() and get_vision_gpu_url()
- Modified bot/utils/image_handling.py to always use NVIDIA for vision
- Enhanced web UI with GPU selector button (blue=NVIDIA, red=AMD)

Files modified:
- docker-compose.yml (added llama-swap-amd service)
- bot/globals.py (added LLAMA_AMD_URL)
- bot/api.py (added GPU selection endpoints and helper function)
- bot/utils/llm.py (GPU routing for text models)
- bot/utils/image_handling.py (GPU routing for vision models)
- bot/static/index.html (GPU selector UI)
- llama-swap-rocm-config.yaml (unified model names)

New files:
- Dockerfile.llamaswap-rocm
- bot/memory/gpu_state.json
- bot/utils/gpu_router.py (load balancing utility)
- setup-dual-gpu.sh (setup verification script)
- DUAL_GPU_*.md (documentation files)
This commit is contained in:
2026-01-09 00:03:59 +02:00
parent ed5994ec78
commit 1fc3d74a5b
21 changed files with 2836 additions and 13 deletions

View File

@@ -635,7 +635,12 @@
<div class="panel">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h1 id="panel-title">Miku Control Panel</h1>
<div style="display: flex; gap: 1rem; align-items: center;">
<h1 id="panel-title">Miku Control Panel</h1>
<button id="gpu-selector-toggle" onclick="toggleGPU()" style="background: #2a5599; color: #fff; padding: 0.5rem 1rem; border: 2px solid #4a7bc9; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.9rem;">
🎮 GPU: NVIDIA
</button>
</div>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<button id="bipolar-mode-toggle" onclick="toggleBipolarMode()" style="background: #333; color: #fff; padding: 0.5rem 1rem; border: 2px solid #666; border-radius: 4px; cursor: pointer; font-weight: bold;">
🔄 Bipolar: OFF
@@ -804,7 +809,33 @@
<!-- Bipolar Mode Section (only visible when bipolar mode is on) -->
<div id="bipolar-section" class="section" style="display: none; border: 2px solid #9932CC; padding: 1rem; border-radius: 8px; background: #1a1a2e;">
<h3 style="color: #9932CC;">🔄 Bipolar Mode Controls</h3>
<p style="font-size: 0.9rem; color: #aaa;">Trigger arguments between Regular Miku and Evil Miku</p>
<p style="font-size: 0.9rem; color: #aaa;">Trigger arguments or dialogues between Regular Miku and Evil Miku</p>
<!-- Persona Dialogue Section -->
<div style="margin-bottom: 2rem; padding: 1rem; background: #252540; border-radius: 8px; border: 1px solid #555;">
<h4 style="color: #6B8EFF; margin-bottom: 0.5rem;">💬 Trigger Persona Dialogue</h4>
<p style="font-size: 0.85rem; color: #999; margin-bottom: 1rem;">Start a natural conversation between the personas (can escalate to argument if tension builds)</p>
<div style="margin-bottom: 1rem;">
<label for="dialogue-message-id">Message ID:</label>
<input type="text" id="dialogue-message-id" placeholder="e.g., 1234567890123456789" style="width: 250px; margin-left: 0.5rem; font-family: monospace;">
</div>
<div style="font-size: 0.8rem; color: #888; margin-bottom: 1rem;">
💡 <strong>Tip:</strong> Right-click any bot response message in Discord and select "Copy Message ID". The opposite persona will analyze it and decide whether to interject.
</div>
<button onclick="triggerPersonaDialogue()" style="background: #6B8EFF; color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer;">
💬 Trigger Dialogue
</button>
<div id="dialogue-status" style="margin-top: 1rem; font-size: 0.9rem;"></div>
</div>
<!-- Argument Section -->
<div style="padding: 1rem; background: #2e1a2e; border-radius: 8px; border: 1px solid #555;">
<h4 style="color: #9932CC; margin-bottom: 0.5rem;">⚔️ Trigger Argument</h4>
<p style="font-size: 0.85rem; color: #999; margin-bottom: 1rem;">Force an immediate argument (bypasses dialogue system)</p>
<div style="margin-bottom: 1rem; display: flex; gap: 1rem; flex-wrap: wrap;">
<div>
@@ -832,6 +863,7 @@
</button>
<div id="bipolar-status" style="margin-top: 1rem; font-size: 0.9rem;"></div>
</div>
<!-- Scoreboard Display -->
<div id="bipolar-scoreboard" style="margin-top: 1.5rem; padding: 1rem; background: #0f0f1e; border-radius: 8px; border: 1px solid #444;">
@@ -1416,6 +1448,7 @@ document.addEventListener('DOMContentLoaded', function() {
loadLogs();
checkEvilModeStatus(); // Check evil mode on load
checkBipolarModeStatus(); // Check bipolar mode on load
checkGPUStatus(); // Check GPU selection on load
console.log('🚀 DOMContentLoaded - initializing figurine subscribers list');
refreshFigurineSubscribers();
loadProfilePictureMetadata();
@@ -2194,6 +2227,59 @@ function updateEvilModeUI() {
updateBipolarToggleVisibility();
}
// GPU Selection Management
let selectedGPU = 'nvidia'; // 'nvidia' or 'amd'
async function checkGPUStatus() {
try {
const response = await fetch('/gpu-status');
if (response.ok) {
const data = await response.json();
selectedGPU = data.gpu || 'nvidia';
updateGPUUI();
}
} catch (error) {
console.error('Failed to check GPU status:', error);
}
}
async function toggleGPU() {
try {
const toggleBtn = document.getElementById('gpu-selector-toggle');
toggleBtn.disabled = true;
toggleBtn.textContent = '⏳ Switching...';
const result = await apiCall('/gpu-select', 'POST', {
gpu: selectedGPU === 'nvidia' ? 'amd' : 'nvidia'
});
selectedGPU = result.gpu;
updateGPUUI();
const gpuName = selectedGPU === 'nvidia' ? 'NVIDIA GTX 1660' : 'AMD RX 6800';
showNotification(`🎮 Switched to ${gpuName}!`);
} catch (error) {
console.error('Failed to toggle GPU:', error);
showNotification('Failed to switch GPU: ' + error.message, 'error');
toggleBtn.disabled = false;
}
}
function updateGPUUI() {
const toggleBtn = document.getElementById('gpu-selector-toggle');
if (selectedGPU === 'amd') {
toggleBtn.textContent = '🎮 GPU: AMD';
toggleBtn.style.background = '#c91432';
toggleBtn.style.borderColor = '#e91436';
} else {
toggleBtn.textContent = '🎮 GPU: NVIDIA';
toggleBtn.style.background = '#2a5599';
toggleBtn.style.borderColor = '#4a7bc9';
}
toggleBtn.disabled = false;
}
// Bipolar Mode Management
let bipolarMode = false;
@@ -2266,6 +2352,48 @@ function updateBipolarToggleVisibility() {
bipolarToggle.style.display = 'block';
}
async function triggerPersonaDialogue() {
const messageIdInput = document.getElementById('dialogue-message-id').value.trim();
const statusDiv = document.getElementById('dialogue-status');
if (!messageIdInput) {
showNotification('Please enter a message ID', 'error');
return;
}
// Validate message ID format (should be numeric)
if (!/^\d+$/.test(messageIdInput)) {
showNotification('Invalid message ID format - should be a number', 'error');
return;
}
try {
statusDiv.innerHTML = '<span style="color: #6B8EFF;">⏳ Analyzing message for dialogue trigger...</span>';
const requestBody = {
message_id: messageIdInput
};
const result = await apiCall('/bipolar-mode/trigger-dialogue', 'POST', requestBody);
if (result.status === 'error') {
statusDiv.innerHTML = `<span style="color: #ff4444;">❌ ${result.message}</span>`;
showNotification(result.message, 'error');
return;
}
statusDiv.innerHTML = `<span style="color: #00ff00;">✅ ${result.message}</span>`;
showNotification(`💬 ${result.message}`);
// Clear the input
document.getElementById('dialogue-message-id').value = '';
} catch (error) {
statusDiv.innerHTML = `<span style="color: #ff4444;">❌ Failed to trigger dialogue: ${error.message}</span>`;
showNotification(`Error: ${error.message}`, 'error');
}
}
async function triggerBipolarArgument() {
const channelIdInput = document.getElementById('bipolar-channel-id').value.trim();
const messageIdInput = document.getElementById('bipolar-message-id').value.trim();