perfkitbenchmarker/linux_benchmarks/dpdk_pktgen_benchmark.py (229 lines of code) (raw):

# Copyright 2025 PerfKitBenchmarker Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Runs DPDK Pktgen benchmarks for high-performance networking. DPDK Pktgen Benchmark is a more feature-rich DPDK benchmark than dpdk_testpmd. DPDK bypasses the kernel networking stack, allowing for much higher PPS. Benchmark Documentation: https://pktgen-dpdk.readthedocs.io/en/latest/getting_started.html https://toonk.io/building-a-high-performance-linux-based-traffic-generator-with-dpdk/index.html """ from typing import Any, Mapping from absl import flags from perfkitbenchmarker import background_tasks from perfkitbenchmarker import benchmark_spec as bm_spec from perfkitbenchmarker import configs from perfkitbenchmarker import linux_virtual_machine from perfkitbenchmarker import sample from perfkitbenchmarker.linux_packages import dpdk_pktgen BENCHMARK_NAME = 'dpdk_pktgen' BENCHMARK_CONFIG = """ dpdk_pktgen: description: Runs dpdk testpmd benchmarks vm_groups: vm_1: vm_spec: *default_dual_core vm_2: vm_spec: *default_dual_core flags: placement_group_style: closest_supported gce_subnet_name: default,dpdk0 gce_nic_types: GVNIC,GVNIC gce_nic_queue_counts: default,default gce_network_type: custom ip_addresses: INTERNAL """ FLAGS = flags.FLAGS _DPDK_PKTGEN_DURATION = flags.DEFINE_integer( 'dpdk_pktgen_duration', 60, 'Run duration in seconds.' ) _DPDK_PKTGEN_PACKET_LOSS_THRESHOLDS = flags.DEFINE_multi_float( 'dpdk_pktgen_packet_loss_threshold_rates', [0, 0.00001, 0.0001], 'Packet loss thresholds to record samples for.', lower_bound=0, ) _DPDK_PKTGEN_NUM_FLOWS = flags.DEFINE_integer( 'dpdk_pktgen_num_flows', 200, 'Number of flows to use by taking a range of source ports.', lower_bound=0, ) # DPDK Pktgen maximum logical cores _MAX_LCORES = 128 _START_RATE = 100000000 # Percent difference in PPS between consecutive iterations to terminate binary # search. _PPS_BINARY_SEARCH_THRESHOLD = 0.01 _STDOUT_LOG_FILE = 'pktgen_stdout.log' def GetConfig(user_config: Mapping[Any, Any]) -> Mapping[Any, Any]: """Merge BENCHMARK_CONFIG with user_config to create benchmark_spec. Args: user_config: user-defined configs (through FLAGS.benchmark_config_file or FLAGS.config_override). Returns: The resulting configs that come from merging user-defined configs with BENCHMARK_CONFIG. """ return configs.LoadConfig(BENCHMARK_CONFIG, user_config, BENCHMARK_NAME) def Prepare(benchmark_spec: bm_spec.BenchmarkSpec) -> None: """Prepares both VM's to run DPDK Pktgen. Args: benchmark_spec: The benchmark specification. """ sender_vm, receiver_vm = benchmark_spec.vms[:2] background_tasks.RunThreaded( lambda vm: vm.Install('dpdk_pktgen'), [sender_vm, receiver_vm], ) background_tasks.RunThreaded( lambda vm: PrepareVM( vm, sender_vm.internal_ips[1], sender_vm.secondary_mac_addr, receiver_vm.internal_ips[1], receiver_vm.secondary_mac_addr, ), [sender_vm, receiver_vm], ) receiver_vm.RemoteCommand( 'sudo sed -i "s/start all//g"' f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' ) # Ensure receiver runs longer than sender. Receiver starts 1 second before # sender, so make receiver duration 2 seconds longer. receiver_vm.RemoteCommand( f'sudo sed -i "s/<DURATION>/{_DPDK_PKTGEN_DURATION.value+2}/g"' f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' ) sender_vm.RemoteCommand( f'sudo sed -i "s/<DURATION>/{_DPDK_PKTGEN_DURATION.value}/g"' f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' ) def PrepareVM( vm: linux_virtual_machine.BaseLinuxVirtualMachine, sender_vm_ip: str, sender_vm_mac: str, receiver_vm_ip: str, receiver_vm_mac: str, ) -> None: """Prepares a VM to run DPDK Pktgen.""" vm.PushDataFile( 'dpdk_pktgen/pktgen.pkt', f'{dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt', ) vm.RemoteCommand( f'sudo sed -i "s/<SRC_IP>/{sender_vm_ip}/g"' f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' ) vm.RemoteCommand( f'sudo sed -i "s/<SRC_MAC>/{sender_vm_mac}/g"' f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' ) # Updating src port numbers for multiple flows. vm.RemoteCommand( 'sudo sed -i "s/1234 1234 1234 0/1234 1234' f' {1233+_DPDK_PKTGEN_NUM_FLOWS.value} 1/g"' f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' ) vm.RemoteCommand( f'sudo sed -i "s/<DST_IP>/{receiver_vm_ip}/g"' f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' ) vm.RemoteCommand( f'sudo sed -i "s/<DST_MAC>/{receiver_vm_mac}/g"' f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' ) # Updating dst port numbers for multiple flows. vm.RemoteCommand( 'sudo sed -i "s/5678 5678 5678 0/5678 5678' f' {5677+_DPDK_PKTGEN_NUM_FLOWS.value} 1/g"' f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/pktgen.pkt' ) def Run(benchmark_spec: bm_spec.BenchmarkSpec) -> list[sample.Sample]: """Runs DPDK benchmarks. Args: benchmark_spec: The benchmark specification. Returns: A list of sample.Sample objects with the performance results. Raises: RunError: A run-stage error raised by an individual benchmark. """ sender_vm, receiver_vm = benchmark_spec.vms[:2] samples = [] num_lcores = min(sender_vm.NumCpusForBenchmark(), _MAX_LCORES) num_memory_channels_stdout, _ = sender_vm.RemoteCommand( "lscpu | grep 'NUMA node(s)' | awk '{print $3}'" ) num_memory_channels = int(num_memory_channels_stdout) metadata = { 'dpdk_pkgen_burst': 1, 'dpdk_pktgen_lcores': num_lcores, 'dpdk_pktgen_num_memory_channels': num_memory_channels, 'dpdk_pktgen_duration': _DPDK_PKTGEN_DURATION.value, 'dpdk_pktgen_num_flows': _DPDK_PKTGEN_NUM_FLOWS.value, } pktgen_env_var = '' # Incorrect llq_policy default: # https://github.com/amzn/amzn-drivers/issues/331 aws_eal_arg = '' if sender_vm.CLOUD == 'AWS': pktgen_env_var = ' LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib64' aws_eal_arg = f' -a "{receiver_vm.secondary_nic_bus_info},llq_policy=1"' cmd = ( f'cd {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR} &&' f' sudo{pktgen_env_var} ./usr/local/bin/pktgen -l 0-{num_lcores-1} -n' f' {num_memory_channels}{aws_eal_arg} -- -m "[1-7].0" -f pktgen.pkt >' f' {_STDOUT_LOG_FILE}' ) prev_rate = _START_RATE for packet_loss_threshold in _DPDK_PKTGEN_PACKET_LOSS_THRESHOLDS.value: metadata = metadata.copy() metadata['dpdk_pktgen_packet_loss_threshold'] = packet_loss_threshold valid_total_sender_tx_pkts = None valid_total_sender_rx_pkts = None valid_total_receiver_rx_pkts = None valid_packet_loss_rate = 1 # Binary search for max PPS under packet loss rate thresholds. prev_pps, curr_pps = -float('inf'), 0 lb, ub = 0, _START_RATE * 2 while ( (abs(curr_pps - prev_pps) / (curr_pps + 1)) > _PPS_BINARY_SEARCH_THRESHOLD ) or (not valid_total_receiver_rx_pkts): curr_rate = (lb + ub) / 2 sender_vm.RemoteCommand( f'sudo sed -i "s/pps = {prev_rate};/pps =' f' {curr_rate};/g"' f' {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/app/pktgen.c' ) sender_vm.RemoteCommand( f'cd {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR} && make' ) # Running Pktgen requires a terminal. background_tasks.RunThreaded( lambda vm: vm.RemoteCommand( cmd, login_shell=True, disable_tty_lock=True ), [receiver_vm, sender_vm], post_task_delay=1, # Ensure receiver starts before sender. ) # Parse ANSI codes. stdout_rx_parser = ( f'cat {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/{_STDOUT_LOG_FILE} |' r' grep -oP "\[7;22H\s*\K[0-9]+" | tail -1' ) stdout_tx_parser = ( f'cat {dpdk_pktgen.DPDK_PKTGEN_GIT_REPO_DIR}/{_STDOUT_LOG_FILE} |' r' grep -oP "\[8;22H\s*\K[0-9]+" | tail -1' ) total_sender_tx_pkts, _ = sender_vm.RemoteCommand(stdout_tx_parser) total_sender_rx_pkts, _ = sender_vm.RemoteCommand(stdout_rx_parser) total_receiver_rx_pkts, _ = receiver_vm.RemoteCommand(stdout_rx_parser) packet_loss_rate = ( int(total_sender_tx_pkts) + int(total_sender_rx_pkts) - int(total_receiver_rx_pkts) ) / int(total_sender_tx_pkts) if packet_loss_rate > packet_loss_threshold: ub = curr_rate else: valid_total_sender_tx_pkts = total_sender_tx_pkts valid_total_sender_rx_pkts = total_sender_rx_pkts valid_total_receiver_rx_pkts = total_receiver_rx_pkts valid_packet_loss_rate = packet_loss_rate lb = curr_rate prev_pps, curr_pps = ( curr_pps, int(total_receiver_rx_pkts) // _DPDK_PKTGEN_DURATION.value, ) prev_rate = curr_rate samples.extend([ sample.Sample( 'Total sender tx packets', int(valid_total_sender_tx_pkts), 'packets', metadata, ), sample.Sample( 'Total sender tx pps', int(valid_total_sender_tx_pkts) // _DPDK_PKTGEN_DURATION.value, 'packets/s', metadata, ), sample.Sample( 'Total sender rx packets', int(valid_total_sender_rx_pkts), 'packets', metadata, ), sample.Sample( 'Total sender rx pps', int(valid_total_sender_rx_pkts) // _DPDK_PKTGEN_DURATION.value, 'packets/s', metadata, ), sample.Sample( 'Total receiver rx packets', int(valid_total_receiver_rx_pkts), 'packets', metadata, ), sample.Sample( 'Total receiver rx pps', int(valid_total_receiver_rx_pkts) // _DPDK_PKTGEN_DURATION.value, 'packets/s', metadata, ), sample.Sample( 'packet loss rate', valid_packet_loss_rate, 'rate (1=100%)', metadata, ), ]) return samples def Cleanup(benchmark_spec: bm_spec.BenchmarkSpec) -> None: """Cleanup benchmarks on the target vm. Args: benchmark_spec: The benchmark specification. """ del benchmark_spec