Batch Generation on WaveSpeed: Jalankan 1.000+ Permintaan Gambar Harian dengan Percaya Diri

Batch Generation on WaveSpeed: Jalankan 1.000+ Permintaan Gambar Harian dengan Percaya Diri

Halo, teman-teman! Saya Dora. Ini dimulai dengan satu keluhan kecil: saya membutuhkan beberapa ratus gambar varian untuk tes, dan loop permintaan tunggal biasa saya terasa seperti mendorong keranjang belanja dengan roda yang macet. Saya terus mendengar bahwa Batch Generation di WaveSpeed dapat menangani volume besar. Saya tidak membutuhkan sesuatu yang spektakuler. Saya hanya ingin pekerjaan terasa lebih ringan.

Jadi selama beberapa sesi di akhir Desember dan minggu ini lagi, saya menyiapkan pipeline batch sederhana di WaveSpeed dan memintanya menjalankan lebih dari 1.000 permintaan gambar. Tidak ada trik khusus, hanya throughput yang stabil, status yang jelas, dan retry yang bersih. Di bawah ini adalah bentuk yang berhasil untuk saya, bagian-bagian yang menjadi hambatan, dan pilihan-pilihan kecil yang membuat biaya dan error tetap terkontrol saat perhatian saya berada di tempat lain.

Gambaran Umum Arsitektur Batch

Producer / Queue / Worker / Storage

Saya sengaja membuat komponen-komponen ini tetap sederhana. Skrip producer kecil mengumpulkan prompt dan metadata, queue menampung job, worker stateless memanggil WaveSpeed image API, dan storage menyimpan hasilnya. Setiap bagian bisa gagal tanpa meruntuhkan seluruh sistem.

  • Producer: Membaca CSV berisi prompt dan pengaturan per gambar (model, ukuran, seed). Menulis satu job per baris ke dalam queue dengan kunci idempotency dan soft deadline.
  • Queue: Saya pernah menggunakan Redis Streams sekali dan RabbitMQ sekali. Keduanya berhasil. Jika Anda belum menjalankan keduanya, Redis lebih ringan untuk memulai.
  • Workers: Proses containerized yang menarik job, memanggil WaveSpeed, menulis hasil ke object storage (saya menggunakan S3), dan memperbarui status. Mereka stateless jadi scaling adalah pengaturan, bukan rebuild.
  • Storage: Satu bucket untuk gambar, satu untuk metadata JSON. Folder sederhana berdasarkan tanggal dan batch ID membuat semuanya tetap rapi.

Yang mengejutkan saya adalah betapa sedikit kode yang berubah saat saya scale dari 100 menjadi 1.200 gambar; sebagian besar masalah tentang pacing dan perlindungan terhadap duplikat, bukan throughput.

Diagram Arsitektur Sederhana

Ini gambar yang saya simpan di kepala:

Producer → Queue → Workers → WaveSpeed API → Storage
             ↓                      ↑
          State DB ←────────→ Metrics/Alerts
  • State DB bisa sama dengan Redis atau tabel Postgres ringan.
  • Metrics memberi alert saat error rate atau biaya terlihat aneh.

Ini tidak rumit. Itu intinya. Saat API mengembalikan 429/5xx sporadis, queue menyerap itu. Saat worker mati di tengah jalan, yang lain mengambilnya setelah visibility timeout.

Strategi Concurrency

Level parallelism yang aman

Pada run saya, saya mulai dengan 5 worker, masing-masing melakukan 2 in-flight request. Itu memberi saya stabil 8–10 gambar/menit tanpa melebihi batas. Bump ke 20 concurrent request berhasil sebentar, lalu saya lihat spike retry. Setting terbaik bukan peak tercepat, tapi average terrata.

Jika Anda mencoba ini: temukan jumlah terkecil worker yang membuat queue tidak bertumbuh. Lalu naikkan perlahan. Amati p95 latency dan error rate selama 10–15 menit sebelum menyentuh apa pun.

Kesadaran rate limit

WaveSpeed menerbitkan panduan rate limit di docs, tapi limit masih berbeda dengan model dan akun. Saya menambahkan dua guardrail:

  • Client-side token bucket: Setiap worker mengakuisisi token sebelum memanggil API. Token terisi ulang pada RPS efektif paket. Saat saya ganti model, saya sesuaikan refill.
  • Backoff discipline: 429 dan 5xx memicu exponential backoff dengan jitter, dibatasi 30 detik. Ini mencegah stampede setelah outage singkat.

Saya juga tag setiap job dengan model + size jadi saya bisa set concurrency ceiling terpisah per model saat dibutuhkan. Ini tidak fancy, hanya switch statement kecil, tapi membantu menghindari hot spot lokal.

Retry & Idempotency

Menghindari gambar duplikat

Batch pertama saya punya bug diam-diam: worker crash setelah API call return, sebelum menulis ke storage. Queue retry job dan saya akhirnya bayar untuk dua generasi identik. Tidak menyenangkan.

Untuk menghentikan ini, saya buat storage path deterministik dari idempotency key dan input hash. Jika retry menemukan gambar sudah ditulis, dia short-circuit dan hanya mark job sebagai success. Fix murah, relief besar.

Implementasi idempotency key

Saya gunakan SHA-256 dari normalized prompt + model + seed + size + guidance. Hash itu menjadi:

  • API idempotency key (dikirim di header atau payload field, tergantung SDK)
  • storage filename prefix
  • database primary key untuk job

Jika API WaveSpeed menghormati idempotency key (cek docs untuk endpoint Anda), repeat call dengan key yang sama return hasil yang sama tanpa charge tambahan. Jika tidak, storage-first check tetap cegah duplikat yang Anda bayar dua kali.

Pemulihan job yang gagal

Tidak setiap failure layak retry lagi. Rule of thumb saya:

  • Retry: 429, 5xx, network timeout, “model busy,” atau transient storage error
  • Jangan retry: 4xx dengan validation error, missing param, atau input jelas jelek

Saya cap retry di 5 dengan exponential backoff. Setelah itu, job landing di dead-letter queue dengan error payload. Sekali sehari, saya triage DLQ job: beberapa dapat fixed input dan requeue, lainnya diarsipkan dengan catatan. Ini jaga overall failure rate di bawah 1.5% untuk run 1.200 gambar.

Job State Management

Status: pending/running/success/failed

Saya coba beberapa bentuk state. Yang paling sederhana bertahan:

  • pending: queued, belum di-lease worker
  • running: di-lease worker dengan lease expiry
  • success: gambar dan metadata ditulis, check pass
  • failed: terminal, dengan error code dan last attempt timestamp

Saya tambah dua optional field yang worth: attempt_count dan last_response_code. Mereka buat dashboard lebih jelas dan debug kurang tebak-tebakan.

Job timeout handling

Dua timeout penting:

  • Lease timeout: Jika worker mati mid-run, job harus return ke pending setelah N detik. Saya gunakan 120s.
  • API timeout: Jika WaveSpeed tidak respond dalam N detik, abort dan retry dengan backoff. Saya gunakan 60s per call.

Saat API lambat, dua ini bisa bertarung. Untuk hindari duplicate work, saya hanya mark running → pending setelah lease expire dan worker heartbeat stop. Heartbeat cuma Redis hash update setiap 10 detik. Jika heartbeat fresh, saya extend lease.

Monitoring & Alerts

Error rate tracking

Saya amati tiga angka selama run:

  • error_rate_5m: rolling 5-minute proporsi failed attempt
  • p95_latency: per-model, per-size
  • retry_depth: berapa banyak job di attempt ≥ 2

Jika error_rate_5m > 5% selama 10 menit, saya potong concurrency setengah otomatis dan kirim note untuk diri saya. Paling spike settle dalam lima menit tanpa fidgeting manual.

Cost spike alerts

Biaya bisa merayap. Saya record:

  • cost_per_image: dilaporkan WaveSpeed jika tersedia, else estimate dari plan
  • duplicate_prevented: count storage short-circuit
  • total_estimated_cost: cumulative

Saat cost_per_image jump lebih dari 30% lawan last hour’s average, saya pause job intake baru dan biarkan queue drain. Dua kali, ini ketahui unintentional parameter change (bigger size, different model) sebelum bill drift. Quiet guardrail seperti ini worth few line code mereka.


Reference Implementation

Python pseudo code
Di bawah adalah bentuk yang saya gunakan. Bukan full code, hanya skeleton:

# producer.py
for row in csv_rows:
    key = hash_inputs(row)
    job = { "id": key, "inputs": row, "deadline": now+6*3600 }
    queue.push(job)

# worker.py
while True:
    job = queue.lease(timeout=120)
    if not job:
        sleep(1)
        continue
    try:
        record_heartbeat(job.id)
        resp = wavespeed.generate_image(inputs=job.inputs, idempotency_key=job.id, timeout=60)
        path = storage_path(job.id, job.inputs)
        if not storage.exists(path):
            storage.write(path, resp.image)
            storage.write(path+'.json', resp.metadata)
        mark_success(job.id)
    except Retryable as e:
        mark_retry(job.id, e)
        backoff_sleep(job.attempt)
    except Fatal as e:
        mark_failed(job.id, e)
    finally:
        queue.release(job)

Node.js pseudo code

// producer.mjs
for (const row of rows) {
    const key = hashInputs(row);
    queue.push({ id: key, inputs: row, deadline: Date.now() + 6 * 3600e3 }); // 6 hours
}


// worker.mjs
while (true) {
    const job = await queue.lease(120); // lease timeout in seconds

    if (!job) { // 原来的 ".job" 改为 "!job"
        await delay(1000);
        continue;
    }

    try {
        await heartbeat(job.id);

        const resp = await wavespeed.generateImage(
            { ...job.inputs, idempotencyKey: job.id },
            { timeout: 60000 } // 60 seconds
        );

        const path = makePath(job.id, job.inputs);

        if (!(await storage.exists(path))) { // 原来的 ".(await ...)" 修正
            await storage.write(path, resp.image);
            await storage.write(path + '.json', resp.metadata);
        }

        await markSuccess(job.id);
    } catch (e) {
        if (isRetryable(e)) {
            await markRetry(job.id, e);
        } else {
            await markFailed(job.id, e);
        }
    } finally {
        await queue.release(job);
    }
}

Rekomendasi Config

  • Mulai kecil: 5–10 concurrent request, lalu naik perlahan. Amati p95 dan error_rate_5m, bukan cuma throughput.
  • Config terpisah per model: concurrency, timeout, dan cost expectation berubah dengan model dan size.
  • Idempotency di mana-mana: key di request, deterministic storage path, dan job table keyed sama value.
  • Heartbeat dan lease: mereka terdengar ribet, tapi mereka save Anda dari phantom duplicate.
  • Dashboard sederhana: 6–8 panel cukup — queue length, success/min, error/min, p95, retry depth, dan cost.

Jika Anda sudah jalankan batch job di tempat lain, ini akan terasa familiar. WaveSpeed tidak butuh rethink, hanya beberapa guardrail hati-hati. Itu yang saya inginkan.

Satu catatan terakhir dari run saya: batch paling mulus adalah yang hampir saya tidak amati. Bukan karena “set and forget,” tapi karena sistem memberitahu saya saat butuh perhatian dan tetap tenang saat tidak. Itu terasa seperti kecepatan yang tepat.

Bagaimana dengan Anda? Apakah Anda pernah batch process gambar dengan WaveSpeed akhir-akhir ini? Apa sweet spot Anda untuk concurrency (saya konsisten di 8–10 sekarang)? Atau Anda pernah ketemu bug sneaky (seperti duplicate charge)? Silakan bagikan setup, pitfall, atau tips Anda di komentar!