Category: Performance Benchmark

  • The Inference Bill Is a Memory-Bandwidth Problem

    You added GPUs to make your LLM serving faster, and tokens-per-second barely moved. The dashboard says the GPUs are 40% utilized. Someone suggests a bigger batch size; latency gets worse. This is the moment most teams misdiagnose, because the mental model — “inference is a compute problem, so add compute” — is wrong for the part of inference that dominates your bill. Decode is bound by memory bandwidth, not FLOPs.

    Two phases, two different bottlenecks

    Autoregressive generation has two phases, and they live in different worlds. Prefill processes the whole prompt at once — lots of parallel matrix math, genuinely compute-bound, the GPU’s happy place. Decode generates one token at a time, and to produce each token the hardware must read the entire model’s weights out of high-bandwidth memory (HBM) again. One token, all the weights, every step. Decode isn’t doing much math per byte it moves; it’s moving a staggering number of bytes to do a little math.

    So the number that predicts decode speed isn’t TFLOPS. It’s HBM bandwidth — and you can estimate the floor with arithmetic, no benchmark required.

    The back-of-envelope that predicts your latency

    For a memory-bound decode, the time to generate one token is roughly the bytes you must read divided by how fast you can read them:

    time_per_token      ≈ (params × bytes_per_param) / HBM_bandwidth
    tokens_per_second   ≈ 1 / time_per_token

    Take a 70B model in FP16 (2 bytes/param) — that’s ~140 GB to read per token. On an 80GB accelerator at ~2.0 TB/s, that’s 140 / 2000 ≈ 70 ms/token: a ceiling near 14 tokens/sec for a single stream, before any compute, kernel, or networking overhead. Not because the GPU can’t do the math — because it has to haul 140 GB across the memory bus for every token.

    Model (FP16) Bytes read / token HBM bandwidth Est. ms / token Est. tok/s (1 stream)
    7B ~14 GB 2.0 TB/s ~7 ms ~140
    13B ~26 GB 2.0 TB/s ~13 ms ~77
    70B ~140 GB 2.0 TB/s ~70 ms ~14
    70B (FP8) ~70 GB 2.0 TB/s ~35 ms ~28
    Single-stream decode ceilings from bandwidth alone. Real systems land below these, but the ranking holds.

    Look at the last two rows. Quantizing the 70B model from FP16 to FP8 halves the bytes read per token and roughly doubles decode throughput — not because you added compute, but because you cut the actual bottleneck in half. That’s the tell that you’re memory-bound: the intervention that helps is the one that moves fewer bytes, not the one that adds more math.

    Why batching helps — until it doesn’t

    If each token read costs 140 GB regardless, the obvious move is to make that read serve many requests at once. That’s exactly what continuous batching does: read the weights once, apply them to a batch of in-flight sequences, amortize the bandwidth across all of them. Throughput climbs beautifully. This is the single highest-leverage lever most teams aren’t fully pulling.

    But batching hits a wall with a name: the KV cache. Every concurrent sequence keeps a per-token cache of keys and values in the same HBM you’re already bandwidth-starved on. Push the batch bigger and the KV cache grows until it evicts, spills, or OOMs — and now you’re memory-capacity bound instead of bandwidth bound. You’ve traded one wall for another. The craft is finding the batch size that maximizes throughput at your latency SLO without tipping into KV-cache thrash.

    What this changes about how you buy and build

    • Spec accelerators by bandwidth and capacity, not headline TFLOPS. For decode-heavy serving, HBM bandwidth and size predict your experience better than peak compute nearly every time.
    • Quantization is a throughput lever, not just a memory-savings trick. Fewer bytes per parameter is fewer bytes read per token. The speedup is the point, not a side effect.
    • KV-cache management is a first-class design concern. Paged attention, cache quantization, and sane max-context limits decide how far batching can take you.
    • Measure at your real batch size and context length. A single-stream benchmark and a saturated multi-tenant server are different machines wearing the same sticker.

    The altitude shift

    The reason this matters beyond the invoice is that it’s a problem altitude question. At low altitude, “inference is slow” gets answered with “buy more GPUs,” and the money goes to compute the workload can’t use. Raise the problem one level — which resource is actually saturated? — and the same symptom points to quantization, batching, and KV-cache strategy instead. The tool is not the transformation: a faster accelerator you’re running memory-blind just reaches the same wall a little sooner.

    Hold all of this as a conviction with a review date. The arithmetic is stable — bytes-per-token doesn’t care about your vendor — but the constants move: bandwidth per dollar, quantization quality, and cache tricks improve every hardware generation. State the model strongly enough to plan a cluster around it, and re-run the numbers when the next accelerator ships.

    The takeaway

    Before you approve another GPU order to fix inference latency, ask which resource is saturated. If decode dominates your workload, the honest answer is usually memory bandwidth — and the fixes that work are the ones that move fewer bytes per token, not the ones that add more math. Compute is what the datasheet sells. Bandwidth is what you actually ship on.

    The token-cost calculator and serving benchmarks are on GitHub: github.com/waghmaredb/vexpose-labs. Running LLMs in production and seeing the same wall? I’d like to compare numbers — LinkedIn or X.

  • Your pgvector Search Gets Slower as You Add Data. Here’s the Setting Everyone Misses.

    Your semantic search was instant at ten thousand rows. At two million it’s 800 milliseconds and climbing, and you never touched the query. Almost always the cause is the same: pgvector is doing an exact, brute-force scan of every vector because its index was never built — or was built with defaults that don’t fit your data.

    Exact search doesn’t scale, and it’s the default

    Without an approximate index, pgvector compares your query vector against every row. That’s fine at ten thousand and fatal at two million. The fix is an ANN index — but an ANN index has two knobs that decide everything, and both have quietly wrong defaults for a large table.

    -- lists ≈ rows / 1000 up to ~1M rows, then ≈ sqrt(rows).
    -- 2,000,000 rows -> ~1,414 lists, NOT the tiny number you'll get by guessing.
    CREATE INDEX ON docs
      USING ivfflat (embedding vector_cosine_ops)
      WITH (lists = 1414);
    
    -- probes trades recall for speed AT QUERY TIME. The default of 1 is far too low.
    -- A sane starting point is ~sqrt(lists); here sqrt(1414) ≈ 38. Then tune to a recall target.
    SET ivfflat.probes = 38;

    Two failure modes, opposite symptoms. Too few lists and each partition is huge, so every probe scans a lot — slow. Too few probes and you scan too few partitions — fast, but you silently miss relevant results, which in a RAG system means confidently answering from the wrong chunks. You cannot tune one without measuring the other.

    Build the index after the data is loaded

    IVFFlat clusters your existing vectors to define its lists. Build it on an empty or tiny table and the clusters are meaningless; every later insert lands in an ill-fitting partition and recall degrades. Load first, then index — and rebuild after any large ingest.

    -- HNSW: slower to build, larger on disk, but better recall/latency
    -- and no clusters to go stale as data changes.
    CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops)
      WITH (m = 16, ef_construction = 64);
    SET hnsw.ef_search = 40;   -- the recall/speed dial at query time

    If your data grows or churns continuously, HNSW usually ages better than IVFFlat because it has no clusters to go stale. It costs more to build and store; that’s the trade you’re making.

    The lesson

    Measure recall, not just latency. A vector search that got ten times faster by missing a third of the right answers isn’t faster — it’s broken with a good p99. Pick a fixed set of queries with known-good results, and every time you touch lists, probes, or the index type, confirm recall held before you celebrate the speed.

    The benchmark harness and recall test are on GitHub: github.com/waghmaredb/vexpose-labs. Tuning vector search at scale? Compare notes on LinkedIn or X.

  • The Benchmark Trap: Why Your Storage Numbers Lie — and How to Get Honest Ones

    A vendor datasheet promises a million IOPS. You buy the array, point your workload at it, and it’s slow. The number wasn’t a lie — it was just irrelevant to you. That gap, between an impressive benchmark and a disappointing production system, is where a lot of infrastructure money quietly goes to die.

    I’ve spent a large part of my career with fio, vdbench, and HammerDB open in front of me. The tools are easy. Getting an honest number out of them is not. Here’s the field guide I wish more teams had before they trusted a benchmark — their own or a vendor’s.

    The number isn’t wrong. It’s answering a different question.

    Peak IOPS on a datasheet is a real measurement — of a workload that looks nothing like yours. Tiny block size, unlimited queue depth, a working set small enough to live entirely in cache, reads only, measured for ten seconds. Your production is a 70/30 read-write mix, 16K blocks, a working set far larger than cache, and it has to stay fast at 2 a.m. on day 400. Same tool, different universe. Before you argue about whose array is faster, make sure you’re both describing the same planet.

    The traps that inflate a benchmark

    Most misleading numbers come from a short list of mistakes:

    • The working set fits in cache. You benchmarked DRAM, not the media. Size the dataset several times larger than the controller cache or you’re measuring the wrong component.
    • No steady state. Flash gets slower once garbage collection kicks in. A 60-second run flatters an SSD that looks very different an hour later. Precondition the device, then measure.
    • Queue-depth theater. Cranking queue depth to 256 maximizes IOPS and obliterates latency. It produces a big number and an unusable response time.
    • Wrong block size or mix. The datasheet uses 4K reads; your database does 8K–16K with real writes. Model the actual mix or the result is fiction.
    • A single sample. One run is an anecdote. The variance across five runs is the actual story.

    An honest fio job to start from

    [global]
    ioengine=libaio
    direct=1
    runtime=600
    time_based=1
    ramp_time=60              # reach steady state before recording
    group_reporting=1
    
    [db-like]
    rw=randrw
    rwmixread=70              # 70/30 read-write, like an OLTP database
    bs=16k                    # your real block size, not 4k
    iodepth=32               # realistic, not a vanity QD of 256
    numjobs=4
    size=200g                # larger than the array's cache
    percentile_list=99:99.9  # report the tail, not just the average

    Every knob here is a decision about honesty. direct=1 bypasses the page cache so you measure storage, not memory. ramp_time throws away the artificially fast warm-up. size forces cache misses. percentile_list is the one most people skip — and it’s the one that matters most.

    Read the right metrics

    Peak IOPS is the vanity metric. What actually predicts whether production will be happy:

    • Latency at your target throughput — not throughput at unlimited latency. Those are different questions with very different answers.
    • Tail latency (p99, p99.9). Your users feel the worst 1% of requests, not the average. A great mean with an ugly tail is a bad system wearing a good costume.
    • Consistency over time. Does it hold at steady state, or degrade as the device fills and ages?

    One reframing kills most bad purchases: fix a latency budget — say, 1 ms at p99 — and ask “how many IOPS can it sustain at or under that?” Suddenly the million-IOPS array and the “slower” one often trade places.

    Why this is a leadership discipline, not a lab chore

    Benchmarking is how you replace opinion with evidence. In a room full of vendor claims and strong personalities, the person holding a reproducible number wins the decision — and deserves to. That’s also why a benchmark is a conviction with a review date: you state a performance expectation strongly enough to plan around it, and you re-run it when the firmware, the workload, or the scale changes. A claim without a benchmark is just an opinion. A benchmark you can’t reproduce is just a different opinion with a chart attached.

    The takeaway

    Don’t ask “how fast is it.” Ask “how fast is it, running my workload, at my latency budget, at steady state, averaged over five runs.” The tools will answer honestly if you ask honestly. Everything else is marketing with a monospaced font.

    The full fio job file is on GitHub: github.com/waghmaredb/vexpose-labs. If you benchmark enterprise storage or databases for a living, I’d like to compare methodologies — reach me on LinkedIn or X.

  • How to Benchmark Enterprise Storage: Fio and Vdbench Explained

    How to Benchmark Enterprise Storage: Fio and Vdbench Explained

    Enterprise storage arrays from vendors like Dell EMC, NetApp, and Pure Storage are the foundation of mission-critical IT environments. Ensuring these systems deliver consistent, high performance under real-world workloads is essential for application reliability and business continuity. In this post, we’ll explore how to benchmark enterprise storage using two industry-leading tools- Fio and Vdbench– with practical configuration examples and best practices.

    Why Benchmarking Enterprise Storage is Unique

    Enterprise arrays are not just fast disks-they’re complex systems with:

    • Multiple controllers and cache layers
    • Advanced data protection (RAID, erasure coding)
    • High-speed protocols (Fibre Channel, iSCSI, NVMe-oF)
    • Storage tiering and virtualization
    • Multi-protocol (block, file) support

    Benchmarking these systems is different from testing a single SSD or HDD. You must simulate production-like workloads, test at scale, and observe performance under both normal and failure conditions.

    Best Practices for Enterprise Storage Benchmarking

    • Simulate real-world workloads: Use realistic mixes of random/sequential I/O, read/write ratios, and block sizes.
    • Test at scale: Ensure test data sets exceed cache sizes and run with sufficient concurrency.
    • Run long-duration tests: Observe steady-state performance, not just short-term cache hits.
    • Monitor the full stack: Track storage, network, and server metrics.
    • Document everything: Record hardware/software versions, configurations, and test parameters.
    • Include failure scenarios: Simulate controller or network failures to test resilience.

    Fio: Flexible I/O Tester

    Fio is a versatile, scriptable tool ideal for generating a wide range of I/O workloads against enterprise storage.

    Example Fio Job File for Enterprise Storage
    text[global]
    ioengine=libaio
    direct=1
    rw=randrw
    rwmixread=70
    bs=8k
    iodepth=64
    numjobs=8
    size=100G
    runtime=3600
    time_based
    group_reporting
    filename=/dev/sdx # Replace with your LUN or device

    [verify]
    verify=crc32

    What this does:

    • Simulates a 70% read/30% write random workload with 8KB blocks.
    • Runs 8 parallel jobs with a queue depth of 64 for 1 hour.
    • Uses direct I/O to bypass the OS cache.
    • Verifies data integrity with CRC32.

    Run it with:

    sudo fio enterprise_test.fio

    Tips:

    • Use multiple devices or files to simulate multi-volume workloads.
    • Adjust concurrency (numjobs, iodepth) to match your environment.
    • Use --output-format=json for detailed reporting.

    Vdbench: Enterprise Workload Generator

    Vdbench is designed for complex, multi-host enterprise storage validation and offers granular workload definition and data validation.

    Example Vdbench Configuration
    text# Storage Definitions for multiple LUNs
    sd=sd1,lun=/dev/sdb,size=100g,openflags=o_direct
    sd=sd2,lun=/dev/sdc,size=100g,openflags=o_direct
    sd=sd3,lun=/dev/sdd,size=100g,openflags=o_direct
    sd=sd4,lun=/dev/sde,size=100g,openflags=o_direct

    # Workload Definition: 67% read, 33% write, 8KB random
    wd=wd1,sd=(sd1-sd4),xfersize=8k,rdpct=67,seekpct=100

    # Run Definition: max I/O, 24 hours, 1s reporting
    rd=rd1,wd=wd1,iorate=max,elapsed=86400,interval=1

    Run it with:

    vdbench -f enterprise_vdbench.conf

    Advanced tips:

    • Use the hd section to define multiple hosts for distributed testing.
    • Simulate failures (e.g., disconnect a path or controller) during the run to observe failover behavior.
    • Use fsd and fwd for NAS/file workloads.

    Key Steps for Success

    1. Profile your workload: Know your application’s I/O patterns. (capture the workload IO pattern)
    2. Prepare your environment: Use dedicated test LUNs/volumes.
    3. Configure your tools: Use Fio or Vdbench job files that match your workload.
    4. Run and monitor: Capture storage, host, and network metrics.
    5. Analyze results: Look for steady-state performance, latency spikes, and the impact of failures.
    6. Document and repeat: Ensure tests are reproducible and results are transparent.

    Conclusion

    Benchmarking enterprise-class storage is about more than just peak numbers-it’s about understanding how your array performs under pressure, during failures, and with your real workloads. Tools like Fio and Vdbench provide the flexibility, power, and validation features needed for accurate, actionable results. By following best practices and using realistic configurations, you can ensure your storage infrastructure is ready for the demands of the modern enterprise.

    References:

    • SNIA Storage Performance Testing Guide
    • Fio and Vdbench Official Documentation
  • Database Performance Benchmark using HammerDB

    Database Performance Benchmark using HammerDB

    I have been using HammerDB for database performance bench-marking . This is very useful tool if you’re getting into any proof of concepts (POC) or testing of new database infrastructure stack before production roll-out.

    In this blog post we will discuss around using HammerDB to generate OLTP (TPC-C workload) on Microsoft SQL database. In my example I had Microsoft SQL and HammerDB server created on AWS EC2 instances. But from HammerDB perspective it doesn’t matter as far as it can communicate to database instance.

    We will follow 5 simple steps to run synthetic workload on MS SQL database. So let’s get started.

    Step 1 – Installing the HammerDB Application

    Before you get started make sure that –

    • HammerDB and MS SQL server are on same network/VLAN
    • MS SQL server is installed and running
    • Login to MS SQL console and create new SQL database. Make sure that new database is created on the target storage disks. If you create database in C drive, then you might not get expected performance due to disk bottleneck.
    Microsoft SQL database console
    • Download HammerDB installer (Click image below)
    HammerDB download link
    • Once HammerDB installer is downloaded, go ahead and install the same. Below are the links for installation steps.

    At this point you should have

    • MS SQL server ready with test database created on desired target storage disks
    • HammerDB installed and running

    Step 2 – Configuration of Schema Build

    We will need to created OLTP workload schema as per TPC-C specifications. Follow below steps for building OLTP schema on target database

    • Open the HammerDB console
    HammerDB – Console
    • Under Benchmark navigation page, double-click on SQL Server. In Benchmark Options pop-up window select SQL Server and TPC-C options. Then click Ok. Click Ok once again to confirm the selection.
    HammerDB – Selecting SQL database and TPC-C workload
    • Under Benchmark navigation now you can see SQL Server selected with TPC-C options.
    HammerDB – SQL server and TPC-C benchmark
    • Expand TPC-C under SQL Server and then expand Schema Build
    HammerDB – TPC-C Schema Build
    • Double-click on Options under Schema Build. Pop-up window of TPC-C Build Options will open. In this window enter below details, and then click Ok
      • SQL Server – IP address or hostname of SQL server. Keep (local) if HammerDB is installed on the SQL server.
      • Authentication – Use Windows Authentication if you want logged-in user credentials to be used for SQL server, else select SQL authentication and enter credentials.
      • SQL Server Database – Name of the database which we had created in Step 1
      • Number of Warehouses – Enter the value to number of warehouses you have chosen for your testing. Preferably select number of warehouses equal to number of cores on the server
      • Virtual Users to Build Schema – Set this value equal to number of warehouses.
    HammerDB – TPC-C Build Options
    • In the Benchmark pane double-click on Build
    HammerDB – TPC-C Schema Build
    • Click Yes to confirm creating schema in the target SQL database.
    • HammerDB will now start creating virtual user threads and create schema in the target database. This process takes some time. You can monitor the status in top-right corner of HammerDB console. (TPC-C creation in top-right)
    HammerDB – TPC-C Schema Creation in process
    • Once completed you can see that the status is changed to Complete
    HammerDB – TPC-C Schema Creation Completed

    Step 3 – Configure Driver Script

    Follow below steps to configure HammerDB driver script

    • Expand the Driver Script from Benchmark navigation pane and double-click on Options
    HammerDB – Driver Script
    • In the TPC-C Driver Options pop-up window enter below details, and then click Ok
      • SQL Server – Keep it same as Step 2
      • Authentication – Keep it same as Step 2
      • Total Transactions Per User – Keep default value. This value will set the number of transactions each virtual user will process before logging off
      • TPC-C Driver Script – Keep this option as Timed Driver Script. This will run the workload for finite time as specified in Minutes for Test Duration
      • Minutes of Rampup Time – The rampup time defines the time in minutes for the monitoring virtual user to wait for the virtual users running the workload to connect to the database. 2 minutes in my case. You can increase this number if you’ve higher number of virtual users.
      • Minutes for Test Duration – The Minutes for Test Duration is shown as duration in the Driver Script. This does not include rampup time.
      • Use All Warehouses – Keep this option checked.
    HammerDB – Driver Script Options
    • In the Benchmark navigation double-click on Load (under Driver Script). You need to Load the Driver Script every time you make changes to Driver Script Options.
    HammerDB – Driver Script Load

    Step 4 – Create Virtual User

    Once HammerDB driver script is loaded, follow below steps to create virtual user.

    • Expand Virtual User from Benchmark pane
    HammerDB – Virtual User
    • Double-click on Options. In the pop-up window enter below details, and then click Ok.
      • Virtual Users – Keep number of users same as Step 2
      • Keep Other inputs as default
    HammerDB – Virtual User Options
    • In the Benchmark navigation pane double-click on Create under Virtual User. This will create virtual users and keep them idle.
    HammerDB – Virtual User Create

    At this point we are ready to start OLTP workload on target database.

    Step 5 – Run HammerDB OLTP (TPC-C) Workload

    Follow below steps to run the OLTP workload and monitor the TPMs.

    • In Benchmark pane double-click on Run (Under Virtual User).
    HammerDB – Run OLTP (TPC-C) workload
    • Now Virtual Users will start logging into the target database and begin running their workload. You can monitor the status under Virtual User 1-MONITOR
    HammerDB – Starting OLTP (TPC-C) Workload – In Progress
    • While workload is running you can monitor the real-time TPM (Transactions Per Minute) by clicking on Transaction Counter
    HammerDB – Transaction Counter
    • Once workload is completed you can see the status in top-right corner as well as under Virtual User 1-MONITOR
    HammerDB – Starting OLTP (TPC-C) Workload – Completed
    • Once the workload is completed note down TEST RESULT under Virtual User 1-MONITOR
    HammerDB – OLTP (TPC-C) TEST RESULT

    You can run multiple tests and take average across them for realistic performance numbers.

    Apart from MS SQL (used in this blog post) HammerDB supports running OLTP (TPC-C) and OLAP (TPC-H) on Oracle, IBM DB2, MySQL, PostgreSQL, MariaDB and Redis.

    I hope this helps everyone.