Add multi-GPU support and HTML test interface
Some checks failed
Build and Push Docker Image / build (push) Failing after 16m23s
Some checks failed
Build and Push Docker Image / build (push) Failing after 16m23s
- Update SageAttention CUDA arch list to support A100, A10, RTX 4090, L40, H100/H200 - Add interactive HTML test page for RunPod API testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,9 +65,10 @@ WORKDIR /tmp
|
||||
ENV EXT_PARALLEL=2
|
||||
ENV NVCC_APPEND_FLAGS="--threads 2"
|
||||
ENV MAX_JOBS=4
|
||||
# Target RunPod GPU architectures: H100/H200(9.0)
|
||||
# Target RunPod GPU architectures:
|
||||
# 8.0 = A100, 8.6 = A10/RTX 3090, 8.9 = RTX 4090/L40, 9.0 = H100/H200
|
||||
# Note: Blackwell (10.0) not yet supported by SageAttention
|
||||
ENV TORCH_CUDA_ARCH_LIST="9.0"
|
||||
ENV TORCH_CUDA_ARCH_LIST="8.0;8.6;8.9;9.0"
|
||||
RUN git clone https://github.com/thu-ml/SageAttention.git && \
|
||||
cd SageAttention && \
|
||||
pip install --no-build-isolation . && \
|
||||
|
||||
612
test-runpod.html
Normal file
612
test-runpod.html
Normal file
@@ -0,0 +1,612 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ComfyUI RunPod Workflow Tester</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #f8f9fa;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
color: #495057;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="number"],
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-upload input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border: 2px dashed #ced4da;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.file-upload-btn:hover {
|
||||
border-color: #667eea;
|
||||
background: #f8f9ff;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.file-upload-btn.has-file {
|
||||
border-color: #28a745;
|
||||
background: #f0fff4;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 300px;
|
||||
margin-top: 15px;
|
||||
border-radius: 6px;
|
||||
display: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-image.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 14px 28px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.status.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.output-section {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.output-video {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin-top: 15px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.json-output {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
margin-top: 15px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎬 ComfyUI RunPod Workflow Tester</h1>
|
||||
<p>Test your image-to-video generation API with custom parameters</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Configuration Section -->
|
||||
<div class="section">
|
||||
<h2>⚙️ RunPod Configuration</h2>
|
||||
<div class="form-group">
|
||||
<label for="endpointId">Endpoint ID</label>
|
||||
<input type="text" id="endpointId" placeholder="abc123xyz">
|
||||
<div class="help-text">Your RunPod serverless endpoint ID (e.g., abc123xyz)</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="apiKey">RunPod API Key</label>
|
||||
<input type="text" id="apiKey" placeholder="YOUR_RUNPOD_API_KEY">
|
||||
<div class="help-text">Your RunPod API key for authentication</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="endpointType">Endpoint Type</label>
|
||||
<select id="endpointType">
|
||||
<option value="runsync" selected>Synchronous (/runsync)</option>
|
||||
<option value="run">Asynchronous (/run)</option>
|
||||
</select>
|
||||
<div class="help-text">Use synchronous for immediate response, async for long-running tasks</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Upload Section -->
|
||||
<div class="section">
|
||||
<h2>🖼️ Input Image</h2>
|
||||
<div class="form-group">
|
||||
<label>Upload Image</label>
|
||||
<div class="file-upload">
|
||||
<input type="file" id="imageInput" accept="image/*">
|
||||
<label for="imageInput" class="file-upload-btn" id="fileUploadBtn">
|
||||
📁 Click to select image
|
||||
</label>
|
||||
</div>
|
||||
<img id="previewImage" class="preview-image" alt="Preview">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameters Section -->
|
||||
<div class="section">
|
||||
<h2>✨ Generation Parameters</h2>
|
||||
<div class="form-group">
|
||||
<label for="prompt">Positive Prompt</label>
|
||||
<textarea id="prompt" placeholder="Describe what you want to see in the video...">a beautiful sunset over the ocean, cinematic lighting</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="negativePrompt">Negative Prompt (Optional)</label>
|
||||
<textarea id="negativePrompt" placeholder="What to avoid...">blurry, low quality, distorted</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label for="resolution">Resolution</label>
|
||||
<select id="resolution">
|
||||
<option value="480">480p</option>
|
||||
<option value="720" selected>720p</option>
|
||||
<option value="1080">1080p</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="steps">Steps</label>
|
||||
<input type="number" id="steps" value="8" min="1" max="50">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label for="splitStep">Split Step</label>
|
||||
<input type="number" id="splitStep" value="4" min="1" max="20">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="timeout">Timeout (seconds)</label>
|
||||
<input type="number" id="timeout" value="600" min="60" max="600">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Section -->
|
||||
<div class="section">
|
||||
<h2>🚀 Execute</h2>
|
||||
<button class="btn btn-primary" id="submitBtn" onclick="submitRequest()">
|
||||
Generate Video
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="clearResults()">
|
||||
Clear Results
|
||||
</button>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Generating video... This may take a few minutes.</p>
|
||||
</div>
|
||||
|
||||
<div class="status" id="status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Output Section -->
|
||||
<div class="section output-section">
|
||||
<h2>📹 Output</h2>
|
||||
<div id="outputContainer"></div>
|
||||
<div id="jsonOutput"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let base64Image = '';
|
||||
|
||||
// Image upload handling
|
||||
document.getElementById('imageInput').addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
const img = document.getElementById('previewImage');
|
||||
img.src = event.target.result;
|
||||
img.classList.add('show');
|
||||
|
||||
// Store base64 without data URL prefix
|
||||
base64Image = event.target.result.split(',')[1];
|
||||
|
||||
const btn = document.getElementById('fileUploadBtn');
|
||||
btn.textContent = '✅ ' + file.name;
|
||||
btn.classList.add('has-file');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
function showStatus(message, type) {
|
||||
const status = document.getElementById('status');
|
||||
status.textContent = message;
|
||||
status.className = 'status show ' + type;
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
const loading = document.getElementById('loading');
|
||||
const btn = document.getElementById('submitBtn');
|
||||
|
||||
if (show) {
|
||||
loading.classList.add('show');
|
||||
btn.disabled = true;
|
||||
} else {
|
||||
loading.classList.remove('show');
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
document.getElementById('outputContainer').innerHTML = '';
|
||||
document.getElementById('jsonOutput').innerHTML = '';
|
||||
document.getElementById('status').classList.remove('show');
|
||||
}
|
||||
|
||||
async function submitRequest() {
|
||||
// Validation
|
||||
const endpointId = document.getElementById('endpointId').value.trim();
|
||||
const apiKey = document.getElementById('apiKey').value.trim();
|
||||
const endpointType = document.getElementById('endpointType').value;
|
||||
|
||||
if (!endpointId) {
|
||||
showStatus('❌ Please enter your RunPod endpoint ID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
showStatus('❌ Please enter your RunPod API key', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct full endpoint URL
|
||||
const endpoint = `https://api.runpod.ai/v2/${endpointId}/${endpointType}`;
|
||||
|
||||
if (!base64Image) {
|
||||
showStatus('❌ Please upload an image first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt = document.getElementById('prompt').value.trim();
|
||||
if (!prompt) {
|
||||
showStatus('❌ Please enter a prompt', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build request payload
|
||||
const payload = {
|
||||
input: {
|
||||
image: base64Image,
|
||||
prompt: prompt,
|
||||
negative_prompt: document.getElementById('negativePrompt').value.trim(),
|
||||
resolution: parseInt(document.getElementById('resolution').value),
|
||||
steps: parseInt(document.getElementById('steps').value),
|
||||
split_step: parseInt(document.getElementById('splitStep').value),
|
||||
timeout: parseInt(document.getElementById('timeout').value)
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading state
|
||||
showLoading(true);
|
||||
showStatus('🚀 Sending request to RunPod...', 'info');
|
||||
clearResults();
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Display raw JSON response
|
||||
document.getElementById('jsonOutput').innerHTML =
|
||||
'<strong>Raw Response:</strong><pre>' +
|
||||
JSON.stringify(result, null, 2) +
|
||||
'</pre>';
|
||||
|
||||
// Handle response
|
||||
if (result.status === 'COMPLETED' && result.output) {
|
||||
showStatus(`✅ Video generated successfully in ${elapsed}s`, 'success');
|
||||
|
||||
const outputContainer = document.getElementById('outputContainer');
|
||||
|
||||
if (result.output.video) {
|
||||
// Base64 video
|
||||
const videoElem = document.createElement('video');
|
||||
videoElem.className = 'output-video';
|
||||
videoElem.controls = true;
|
||||
videoElem.autoplay = true;
|
||||
videoElem.loop = true;
|
||||
videoElem.src = 'data:video/mp4;base64,' + result.output.video;
|
||||
outputContainer.appendChild(videoElem);
|
||||
} else if (result.output.image) {
|
||||
// Base64 image
|
||||
const imgElem = document.createElement('img');
|
||||
imgElem.className = 'output-video';
|
||||
imgElem.src = 'data:image/png;base64,' + result.output.image;
|
||||
outputContainer.appendChild(imgElem);
|
||||
} else if (result.output.file_path) {
|
||||
// File path (large output)
|
||||
const fileInfo = document.createElement('div');
|
||||
fileInfo.className = 'status info show';
|
||||
fileInfo.innerHTML = `
|
||||
<strong>Large output saved to:</strong><br>
|
||||
<code>${result.output.file_path}</code><br><br>
|
||||
<em>File is too large to display (>10MB). Access it on your RunPod volume.</em>
|
||||
`;
|
||||
outputContainer.appendChild(fileInfo);
|
||||
}
|
||||
} else if (result.status === 'FAILED') {
|
||||
showStatus('❌ Generation failed: ' + (result.error || 'Unknown error'), 'error');
|
||||
} else {
|
||||
showStatus('⚠️ Unexpected response status: ' + result.status, 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showStatus('❌ Error: ' + error.message, 'error');
|
||||
console.error('Request failed:', error);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration to localStorage
|
||||
document.getElementById('endpointId').addEventListener('change', function(e) {
|
||||
localStorage.setItem('runpod_endpoint_id', e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('apiKey').addEventListener('change', function(e) {
|
||||
localStorage.setItem('runpod_apikey', e.target.value);
|
||||
});
|
||||
|
||||
document.getElementById('endpointType').addEventListener('change', function(e) {
|
||||
localStorage.setItem('runpod_endpoint_type', e.target.value);
|
||||
});
|
||||
|
||||
// Load saved configuration
|
||||
window.addEventListener('load', function() {
|
||||
const savedEndpointId = localStorage.getItem('runpod_endpoint_id');
|
||||
const savedApiKey = localStorage.getItem('runpod_apikey');
|
||||
const savedEndpointType = localStorage.getItem('runpod_endpoint_type');
|
||||
|
||||
if (savedEndpointId) {
|
||||
document.getElementById('endpointId').value = savedEndpointId;
|
||||
}
|
||||
|
||||
if (savedApiKey) {
|
||||
document.getElementById('apiKey').value = savedApiKey;
|
||||
}
|
||||
|
||||
if (savedEndpointType) {
|
||||
document.getElementById('endpointType').value = savedEndpointType;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user