max / everycycle
5 files changed,
+354 insertions,
-69 deletions
| @@ -36,26 +36,202 @@ | |||
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | 38 | /// Read a representative CPU model line from `/proc/cpuinfo`. | |
| 39 | - | /// | |
| 40 | - | /// x86_64 uses `model name`. Aarch64 typically does not — different | |
| 41 | - | /// kernels and SoCs surface the identifier under `Model`, `Hardware`, | |
| 42 | - | /// or `CPU part`. Walk the candidates in order; return the first hit. | |
| 43 | 39 | fn read_cpu_model() -> String { | |
| 44 | - | const KEYS: &[&str] = &["model name", "Model", "Hardware", "CPU part"]; | |
| 45 | - | let Ok(content) = fs::read_to_string("/proc/cpuinfo") else { | |
| 46 | - | return UNKNOWN.to_string(); | |
| 47 | - | }; | |
| 48 | - | for line in content.lines() { | |
| 49 | - | for key in KEYS { | |
| 50 | - | if let Some(rest) = line.strip_prefix(key) | |
| 51 | - | && let Some(value) = rest.split(':').nth(1) | |
| 52 | - | { | |
| 53 | - | let trimmed = value.trim(); | |
| 54 | - | if !trimmed.is_empty() { | |
| 55 | - | return trimmed.to_string(); | |
| 56 | - | } | |
| 57 | - | } | |
| 40 | + | fs::read_to_string("/proc/cpuinfo") | |
| 41 | + | .map_or_else(|_| UNKNOWN.to_string(), |c| parse_cpu_model(&c)) | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | /// Derive a human-readable CPU model from the contents of `/proc/cpuinfo`. | |
| 45 | + | /// | |
| 46 | + | /// x86_64 supplies `model name`. Aarch64 does not: the kernel exposes | |
| 47 | + | /// the MIDR fields (`CPU implementer`, `CPU part`) and leaves naming to | |
| 48 | + | /// userspace, though board-level kernels often add `Model` or | |
| 49 | + | /// `Hardware`. Prose keys are preferred in order; the MIDR decode is | |
| 50 | + | /// the fallback. | |
| 51 | + | /// | |
| 52 | + | /// The raw implementer and part are always kept in the decoded string. | |
| 53 | + | /// This value is identity metadata in a signed report, so a reader has | |
| 54 | + | /// to be able to check the decode rather than take its word for it. | |
| 55 | + | fn parse_cpu_model(cpuinfo: &str) -> String { | |
| 56 | + | const PROSE_KEYS: &[&str] = &["model name", "Model", "Hardware"]; | |
| 57 | + | for key in PROSE_KEYS { | |
| 58 | + | if let Some(value) = field(cpuinfo, key) { | |
| 59 | + | return value; | |
| 58 | 60 | } | |
| 59 | 61 | } | |
| 60 | - | UNKNOWN.to_string() | |
| 62 | + | decode_midr(cpuinfo).unwrap_or_else(|| UNKNOWN.to_string()) | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | /// First non-empty `key : value` line in `/proc/cpuinfo`. | |
| 66 | + | fn field(cpuinfo: &str, key: &str) -> Option<String> { | |
| 67 | + | cpuinfo | |
| 68 | + | .lines() | |
| 69 | + | .filter_map(|line| { | |
| 70 | + | let (name, value) = line.split_once(':')?; | |
| 71 | + | (name.trim() == key).then(|| value.trim().to_string()) | |
| 72 | + | }) | |
| 73 | + | .find(|value| !value.is_empty()) | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | /// ARM implementer byte to vendor name. Only entries we are confident | |
| 77 | + | /// of; an unrecognized implementer reports its raw byte rather than a | |
| 78 | + | /// guess. | |
| 79 | + | fn implementer_name(implementer: u32) -> Option<&'static str> { | |
| 80 | + | Some(match implementer { | |
| 81 | + | 0x41 => "ARM", | |
| 82 | + | 0x42 => "Broadcom", | |
| 83 | + | 0x43 => "Cavium", | |
| 84 | + | 0x4e => "NVIDIA", | |
| 85 | + | 0x50 => "Applied Micro", | |
| 86 | + | 0x51 => "Qualcomm", | |
| 87 | + | 0x53 => "Samsung", | |
| 88 | + | 0x61 => "Apple", | |
| 89 | + | 0xc0 => "Ampere", | |
| 90 | + | _ => return None, | |
| 91 | + | }) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | /// ARM-designed core names by part number. Deliberately partial: a | |
| 95 | + | /// wrong core name in a signed report is worse than an honest | |
| 96 | + | /// "unknown core". | |
| 97 | + | fn core_name(implementer: u32, part: u32) -> Option<&'static str> { | |
| 98 | + | if implementer != 0x41 { | |
| 99 | + | return None; | |
| 100 | + | } | |
| 101 | + | Some(match part { | |
| 102 | + | 0xd03 => "Cortex-A53", | |
| 103 | + | 0xd05 => "Cortex-A55", | |
| 104 | + | 0xd07 => "Cortex-A57", | |
| 105 | + | 0xd08 => "Cortex-A72", | |
| 106 | + | 0xd09 => "Cortex-A73", | |
| 107 | + | 0xd0b => "Cortex-A76", | |
| 108 | + | 0xd0c => "Neoverse-N1", | |
| 109 | + | 0xd40 => "Neoverse-V1", | |
| 110 | + | 0xd49 => "Neoverse-N2", | |
| 111 | + | 0xd4f => "Neoverse-V2", | |
| 112 | + | _ => return None, | |
| 113 | + | }) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | /// Decode the aarch64 MIDR fields into a readable name, keeping the | |
| 117 | + | /// raw ids for verification. | |
| 118 | + | fn decode_midr(cpuinfo: &str) -> Option<String> { | |
| 119 | + | let implementer_raw = field(cpuinfo, "CPU implementer")?; | |
| 120 | + | let part_raw = field(cpuinfo, "CPU part")?; | |
| 121 | + | let implementer = parse_hex(&implementer_raw)?; | |
| 122 | + | let part = parse_hex(&part_raw)?; | |
| 123 | + | ||
| 124 | + | let ids = format!("{implementer_raw}:{part_raw}"); | |
| 125 | + | Some( | |
| 126 | + | match (implementer_name(implementer), core_name(implementer, part)) { | |
| 127 | + | (Some(vendor), Some(core)) => format!("{vendor} {core} ({ids})"), | |
| 128 | + | (Some(vendor), None) => format!("{vendor} unknown core ({ids})"), | |
| 129 | + | (None, _) => format!("unknown implementer ({ids})"), | |
| 130 | + | }, | |
| 131 | + | ) | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | fn parse_hex(value: &str) -> Option<u32> { | |
| 135 | + | let digits = value | |
| 136 | + | .trim() | |
| 137 | + | .strip_prefix("0x") | |
| 138 | + | .or_else(|| value.trim().strip_prefix("0X")) | |
| 139 | + | .unwrap_or(value.trim()); | |
| 140 | + | u32::from_str_radix(digits, 16).ok() | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | #[cfg(test)] | |
| 144 | + | mod tests { | |
| 145 | + | use super::*; | |
| 146 | + | ||
| 147 | + | /// Trimmed from astra (Ampere Altra, Thelio Astra). No `model name` | |
| 148 | + | /// anywhere in the file, which is what made the raw part number leak | |
| 149 | + | /// into reports. | |
| 150 | + | const AARCH64_ALTRA: &str = "\ | |
| 151 | + | processor\t: 0 | |
| 152 | + | BogoMIPS\t: 50.00 | |
| 153 | + | Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 | |
| 154 | + | CPU implementer\t: 0x41 | |
| 155 | + | CPU architecture: 8 | |
| 156 | + | CPU variant\t: 0x3 | |
| 157 | + | CPU part\t: 0xd0c | |
| 158 | + | CPU revision\t: 1 | |
| 159 | + | "; | |
| 160 | + | ||
| 161 | + | const X86_64: &str = "\ | |
| 162 | + | processor\t: 0 | |
| 163 | + | vendor_id\t: GenuineIntel | |
| 164 | + | model\t\t: 186 | |
| 165 | + | model name\t: 13th Gen Intel(R) Core(TM) i7-1370P | |
| 166 | + | stepping\t: 2 | |
| 167 | + | "; | |
| 168 | + | ||
| 169 | + | const RASPBERRY_PI: &str = "\ | |
| 170 | + | processor\t: 0 | |
| 171 | + | BogoMIPS\t: 108.00 | |
| 172 | + | CPU implementer\t: 0x41 | |
| 173 | + | CPU part\t: 0xd08 | |
| 174 | + | Hardware\t: BCM2835 | |
| 175 | + | Model\t\t: Raspberry Pi 4 Model B Rev 1.4 | |
| 176 | + | "; | |
| 177 | + | ||
| 178 | + | #[test] | |
| 179 | + | fn aarch64_midr_decodes_to_a_core_name() { | |
| 180 | + | assert_eq!( | |
| 181 | + | parse_cpu_model(AARCH64_ALTRA), | |
| 182 | + | "ARM Neoverse-N1 (0x41:0xd0c)" | |
| 183 | + | ); | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | #[test] | |
| 187 | + | fn x86_prefers_the_model_name_line() { | |
| 188 | + | assert_eq!( | |
| 189 | + | parse_cpu_model(X86_64), | |
| 190 | + | "13th Gen Intel(R) Core(TM) i7-1370P" | |
| 191 | + | ); | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | #[test] | |
| 195 | + | fn prose_keys_win_over_the_midr_decode() { | |
| 196 | + | // `Model` outranks `Hardware`, and both outrank the MIDR, even | |
| 197 | + | // though the MIDR lines come first in the file. The old | |
| 198 | + | // line-major scan returned whichever key appeared earliest. | |
| 199 | + | assert_eq!( | |
| 200 | + | parse_cpu_model(RASPBERRY_PI), | |
| 201 | + | "Raspberry Pi 4 Model B Rev 1.4" | |
| 202 | + | ); | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | #[test] | |
| 206 | + | fn unknown_part_stays_honest() { | |
| 207 | + | let unknown_core = AARCH64_ALTRA.replace("0xd0c", "0xd8e"); | |
| 208 | + | assert_eq!( | |
| 209 | + | parse_cpu_model(&unknown_core), | |
| 210 | + | "ARM unknown core (0x41:0xd8e)" | |
| 211 | + | ); | |
| 212 | + | ||
| 213 | + | let unknown_vendor = AARCH64_ALTRA.replace("0x41", "0x69"); | |
| 214 | + | assert_eq!( | |
| 215 | + | parse_cpu_model(&unknown_vendor), | |
| 216 | + | "unknown implementer (0x69:0xd0c)" | |
| 217 | + | ); | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | #[test] | |
| 221 | + | fn nothing_recognizable_reports_unknown() { | |
| 222 | + | assert_eq!(parse_cpu_model(""), UNKNOWN); | |
| 223 | + | assert_eq!(parse_cpu_model("processor\t: 0\n"), UNKNOWN); | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | #[test] | |
| 227 | + | fn empty_values_do_not_win() { | |
| 228 | + | let blank = "model name\t:\nModel\t\t: Thelio Astra\n"; | |
| 229 | + | assert_eq!(parse_cpu_model(blank), "Thelio Astra"); | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | #[test] | |
| 233 | + | fn values_containing_colons_survive() { | |
| 234 | + | let clocked = "model name\t: Some CPU @ 3.00GHz: turbo\n"; | |
| 235 | + | assert_eq!(parse_cpu_model(clocked), "Some CPU @ 3.00GHz: turbo"); | |
| 236 | + | } | |
| 61 | 237 | } |
| @@ -7,8 +7,8 @@ | |||
| 7 | 7 | ||
| 8 | 8 | use everycycle_appraise::enumerate::enumerate; | |
| 9 | 9 | use everycycle_appraise::probe::{ | |
| 10 | - | BandwidthError, LaunchError, VulkanContext, VulkanContextError, measure_bandwidth, | |
| 11 | - | measure_launch_overhead, | |
| 10 | + | BandwidthError, LaunchError, TimingSource, VulkanContext, VulkanContextError, | |
| 11 | + | measure_bandwidth, measure_launch_overhead, | |
| 12 | 12 | }; | |
| 13 | 13 | ||
| 14 | 14 | fn main() { | |
| @@ -53,11 +53,19 @@ | |||
| 53 | 53 | Ok(r) => { | |
| 54 | 54 | eprintln!(" read: {:>7.1} GB/s", r.read_bps / 1e9); | |
| 55 | 55 | eprintln!(" write: {:>7.1} GB/s", r.write_bps / 1e9); | |
| 56 | + | eprintln!(" clock: {}", timing_label(r.timing)); | |
| 56 | 57 | } | |
| 57 | 58 | Err(e) => eprintln!(" failed: {}", render_bw_err(&e)), | |
| 58 | 59 | } | |
| 59 | 60 | } | |
| 60 | 61 | ||
| 62 | + | fn timing_label(t: TimingSource) -> &'static str { | |
| 63 | + | match t { | |
| 64 | + | TimingSource::DeviceTimestamps => "device timestamps", | |
| 65 | + | TimingSource::HostWallClock => "host wall clock (lower bound)", | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | ||
| 61 | 69 | fn render_ctx_err(e: &VulkanContextError) -> String { | |
| 62 | 70 | format!("{e:?}") | |
| 63 | 71 | } |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | //! | |
| 3 | 3 | //! Allocates three device-local buffers (A, B, C), dispatches a | |
| 4 | 4 | //! compute shader that computes `c[i] = a[i] + s * b[i]` for `i` in | |
| 5 | - | //! `[0, N)`, times the dispatch loop, and reports observed read and | |
| 5 | + | //! `[0, N)`, times the dispatches, and reports observed read and | |
| 6 | 6 | //! write bandwidth from the triad: | |
| 7 | 7 | //! | |
| 8 | 8 | //! ```text | |
| @@ -13,6 +13,21 @@ | |||
| 13 | 13 | //! Two reads per element (A and B), one write (C). The shader is a | |
| 14 | 14 | //! pure streaming kernel so the achieved figure tracks the device's | |
| 15 | 15 | //! DRAM throughput, not its cache or compute path. | |
| 16 | + | //! | |
| 17 | + | //! # Timing | |
| 18 | + | //! | |
| 19 | + | //! The dispatch is timed **on the device**, with a timestamp query | |
| 20 | + | //! written either side of it inside the command buffer, and only the | |
| 21 | + | //! deltas are accumulated. Queue submission and the fence wait cost | |
| 22 | + | //! tens of microseconds against a dispatch of a few hundred, so timing | |
| 23 | + | //! the host-side loop instead charges that overhead to the memory | |
| 24 | + | //! system and understates bandwidth by low double-digit percent — | |
| 25 | + | //! measured at ~13% on an RTX 5070 Ti. | |
| 26 | + | //! | |
| 27 | + | //! A queue family that reports no valid timestamp bits falls back to | |
| 28 | + | //! host wall-clock timing. The result records which clock produced it, | |
| 29 | + | //! because the two are not comparable and a report must not imply they | |
| 30 | + | //! are. | |
| 16 | 31 | ||
| 17 | 32 | use std::time::Instant; | |
| 18 | 33 | ||
| @@ -58,10 +73,22 @@ | |||
| 58 | 73 | const WARMUP_ITERATIONS: u32 = 4; | |
| 59 | 74 | const MEASURE_ITERATIONS: u32 = 32; | |
| 60 | 75 | ||
| 76 | + | /// Which clock produced a measurement. | |
| 77 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 78 | + | pub enum TimingSource { | |
| 79 | + | /// Timestamp queries around the dispatch. Excludes submit and | |
| 80 | + | /// fence-wait overhead. | |
| 81 | + | DeviceTimestamps, | |
| 82 | + | /// Host wall clock around submit-and-wait. Includes per-dispatch | |
| 83 | + | /// submission overhead, so the figure is a lower bound. | |
| 84 | + | HostWallClock, | |
| 85 | + | } | |
| 86 | + | ||
| 61 | 87 | #[derive(Clone, Copy, Debug)] | |
| 62 | 88 | pub struct BandwidthResult { | |
| 63 | 89 | pub read_bps: f64, | |
| 64 | 90 | pub write_bps: f64, | |
| 91 | + | pub timing: TimingSource, | |
| 65 | 92 | } | |
| 66 | 93 | ||
| 67 | 94 | #[derive(Debug)] | |
| @@ -106,8 +133,8 @@ | |||
| 106 | 133 | ||
| 107 | 134 | unsafe { destroy_buffers(device, &buffers) }; | |
| 108 | 135 | ||
| 109 | - | let elapsed = result?; | |
| 110 | - | Ok(compute_bandwidth(elapsed, MEASURE_ITERATIONS)) | |
| 136 | + | let (elapsed, timing) = result?; | |
| 137 | + | Ok(compute_bandwidth(elapsed, MEASURE_ITERATIONS, timing)) | |
| 111 | 138 | } | |
| 112 | 139 | ||
| 113 | 140 | fn compile_triad_shader() -> Result<Vec<u32>, BandwidthError> { | |
| @@ -177,7 +204,7 @@ | |||
| 177 | 204 | ctx: &VulkanContext, | |
| 178 | 205 | spirv: &[u32], | |
| 179 | 206 | buffers: &TriadBuffers, | |
| 180 | - | ) -> Result<f64, BandwidthError> { | |
| 207 | + | ) -> Result<(f64, TimingSource), BandwidthError> { | |
| 181 | 208 | let device = &ctx.device; | |
| 182 | 209 | ||
| 183 | 210 | // Descriptor set layout: three storage-buffer bindings. | |
| @@ -257,35 +284,58 @@ | |||
| 257 | 284 | let cmd = | |
| 258 | 285 | unsafe { device.allocate_command_buffers(&cmd_info) }.map_err(BandwidthError::Vulkan)?[0]; | |
| 259 | 286 | ||
| 287 | + | // A queue that cannot write timestamps leaves only the host clock. | |
| 288 | + | let timing = if ctx.timestamp_valid_bits > 0 { | |
| 289 | + | TimingSource::DeviceTimestamps | |
| 290 | + | } else { | |
| 291 | + | TimingSource::HostWallClock | |
| 292 | + | }; | |
| 293 | + | ||
| 294 | + | let query_pool = if timing == TimingSource::DeviceTimestamps { | |
| 295 | + | let info = vk::QueryPoolCreateInfo::default() | |
| 296 | + | .query_type(vk::QueryType::TIMESTAMP) | |
| 297 | + | .query_count(2); | |
| 298 | + | Some(unsafe { device.create_query_pool(&info, None) }.map_err(BandwidthError::Vulkan)?) | |
| 299 | + | } else { | |
| 300 | + | None | |
| 301 | + | }; | |
| 302 | + | ||
| 260 | 303 | let begin = vk::CommandBufferBeginInfo::default(); | |
| 261 | 304 | let groups = ELEMENTS.div_ceil(WORKGROUP_SIZE); | |
| 262 | 305 | ||
| 263 | - | let record = || -> Result<(), BandwidthError> { | |
| 264 | - | unsafe { | |
| 265 | - | device | |
| 266 | - | .begin_command_buffer(cmd, &begin) | |
| 267 | - | .map_err(BandwidthError::Vulkan)?; | |
| 268 | - | device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::COMPUTE, pipeline); | |
| 269 | - | device.cmd_bind_descriptor_sets( | |
| 270 | - | cmd, | |
| 271 | - | vk::PipelineBindPoint::COMPUTE, | |
| 272 | - | pipeline_layout, | |
| 273 | - | 0, | |
| 274 | - | &[descriptor_set], | |
| 275 | - | &[], | |
| 276 | - | ); | |
| 277 | - | device.cmd_dispatch(cmd, groups, 1, 1); | |
| 278 | - | device | |
| 279 | - | .end_command_buffer(cmd) | |
| 280 | - | .map_err(BandwidthError::Vulkan)?; | |
| 306 | + | // Recorded once and resubmitted: the buffer carries no | |
| 307 | + | // ONE_TIME_SUBMIT flag, so re-recording per iteration bought | |
| 308 | + | // nothing and put another host-side cost inside the timed region. | |
| 309 | + | // The query-pool reset lives in the command buffer so each | |
| 310 | + | // submission overwrites the previous pair. | |
| 311 | + | unsafe { | |
| 312 | + | device | |
| 313 | + | .begin_command_buffer(cmd, &begin) | |
| 314 | + | .map_err(BandwidthError::Vulkan)?; | |
| 315 | + | if let Some(pool) = query_pool { | |
| 316 | + | device.cmd_reset_query_pool(cmd, pool, 0, 2); | |
| 317 | + | device.cmd_write_timestamp(cmd, vk::PipelineStageFlags::TOP_OF_PIPE, pool, 0); | |
| 281 | 318 | } | |
| 282 | - | Ok(()) | |
| 283 | - | }; | |
| 319 | + | device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::COMPUTE, pipeline); | |
| 320 | + | device.cmd_bind_descriptor_sets( | |
| 321 | + | cmd, | |
| 322 | + | vk::PipelineBindPoint::COMPUTE, | |
| 323 | + | pipeline_layout, | |
| 324 | + | 0, | |
| 325 | + | &[descriptor_set], | |
| 326 | + | &[], | |
| 327 | + | ); | |
| 328 | + | device.cmd_dispatch(cmd, groups, 1, 1); | |
| 329 | + | if let Some(pool) = query_pool { | |
| 330 | + | device.cmd_write_timestamp(cmd, vk::PipelineStageFlags::BOTTOM_OF_PIPE, pool, 1); | |
| 331 | + | } | |
| 332 | + | device | |
| 333 | + | .end_command_buffer(cmd) | |
| 334 | + | .map_err(BandwidthError::Vulkan)?; | |
| 335 | + | } | |
| 284 | 336 | ||
| 285 | - | record()?; | |
| 286 | - | ||
| 287 | - | let submit = |buffer: vk::CommandBuffer| -> Result<(), BandwidthError> { | |
| 288 | - | let info = vk::SubmitInfo::default().command_buffers(std::slice::from_ref(&buffer)); | |
| 337 | + | let submit = || -> Result<(), BandwidthError> { | |
| 338 | + | let info = vk::SubmitInfo::default().command_buffers(std::slice::from_ref(&cmd)); | |
| 289 | 339 | unsafe { | |
| 290 | 340 | device | |
| 291 | 341 | .queue_submit(ctx.queue, &[info], vk::Fence::null()) | |
| @@ -297,31 +347,60 @@ | |||
| 297 | 347 | Ok(()) | |
| 298 | 348 | }; | |
| 299 | 349 | ||
| 350 | + | // Ticks are only meaningful in the low `timestamp_valid_bits`, so | |
| 351 | + | // mask before subtracting or a wrapped counter reads as a huge | |
| 352 | + | // interval. | |
| 353 | + | let tick_mask: u64 = if ctx.timestamp_valid_bits >= 64 { | |
| 354 | + | u64::MAX | |
| 355 | + | } else { | |
| 356 | + | (1_u64 << ctx.timestamp_valid_bits) - 1 | |
| 357 | + | }; | |
| 358 | + | ||
| 359 | + | let read_dispatch_ns = |pool: vk::QueryPool| -> Result<f64, BandwidthError> { | |
| 360 | + | let mut ticks = [0_u64; 2]; | |
| 361 | + | unsafe { | |
| 362 | + | device | |
| 363 | + | .get_query_pool_results( | |
| 364 | + | pool, | |
| 365 | + | 0, | |
| 366 | + | &mut ticks, | |
| 367 | + | vk::QueryResultFlags::TYPE_64 | vk::QueryResultFlags::WAIT, | |
| 368 | + | ) | |
| 369 | + | .map_err(BandwidthError::Vulkan)?; | |
| 370 | + | } | |
| 371 | + | let delta = (ticks[1] & tick_mask).wrapping_sub(ticks[0] & tick_mask) & tick_mask; | |
| 372 | + | #[allow(clippy::cast_precision_loss)] | |
| 373 | + | Ok(delta as f64 * f64::from(ctx.timestamp_period_ns)) | |
| 374 | + | }; | |
| 375 | + | ||
| 300 | 376 | // Warm-up: drive the device into steady state so the measured pass | |
| 301 | 377 | // reflects thermal-stable behavior, not first-launch overhead. | |
| 302 | 378 | for _ in 0..WARMUP_ITERATIONS { | |
| 303 | - | submit(cmd)?; | |
| 304 | - | unsafe { | |
| 305 | - | device | |
| 306 | - | .reset_command_buffer(cmd, vk::CommandBufferResetFlags::empty()) | |
| 307 | - | .map_err(BandwidthError::Vulkan)?; | |
| 308 | - | } | |
| 309 | - | record()?; | |
| 379 | + | submit()?; | |
| 310 | 380 | } | |
| 311 | 381 | ||
| 312 | - | let start = Instant::now(); | |
| 313 | - | for _ in 0..MEASURE_ITERATIONS { | |
| 314 | - | submit(cmd)?; | |
| 315 | - | unsafe { | |
| 316 | - | device | |
| 317 | - | .reset_command_buffer(cmd, vk::CommandBufferResetFlags::empty()) | |
| 318 | - | .map_err(BandwidthError::Vulkan)?; | |
| 382 | + | let result = match query_pool { | |
| 383 | + | Some(pool) => { | |
| 384 | + | let mut total_ns = 0.0_f64; | |
| 385 | + | for _ in 0..MEASURE_ITERATIONS { | |
| 386 | + | submit()?; | |
| 387 | + | total_ns += read_dispatch_ns(pool)?; | |
| 388 | + | } | |
| 389 | + | total_ns / 1e9 | |
| 319 | 390 | } | |
| 320 | - | record()?; | |
| 321 | - | } | |
| 322 | - | let elapsed = start.elapsed().as_secs_f64(); | |
| 391 | + | None => { | |
| 392 | + | let start = Instant::now(); | |
| 393 | + | for _ in 0..MEASURE_ITERATIONS { | |
| 394 | + | submit()?; | |
| 395 | + | } | |
| 396 | + | start.elapsed().as_secs_f64() | |
| 397 | + | } | |
| 398 | + | }; | |
| 323 | 399 | ||
| 324 | 400 | unsafe { | |
| 401 | + | if let Some(pool) = query_pool { | |
| 402 | + | device.destroy_query_pool(pool, None); | |
| 403 | + | } | |
| 325 | 404 | device.free_command_buffers(ctx.command_pool, &[cmd]); | |
| 326 | 405 | device.destroy_descriptor_pool(descriptor_pool, None); | |
| 327 | 406 | device.destroy_pipeline(pipeline, None); | |
| @@ -330,16 +409,21 @@ | |||
| 330 | 409 | device.destroy_descriptor_set_layout(dsl, None); | |
| 331 | 410 | } | |
| 332 | 411 | ||
| 333 | - | Ok(elapsed) | |
| 412 | + | Ok((result, timing)) | |
| 334 | 413 | } | |
| 335 | 414 | ||
| 336 | 415 | #[allow(clippy::cast_precision_loss)] | |
| 337 | - | fn compute_bandwidth(elapsed_seconds: f64, iterations: u32) -> BandwidthResult { | |
| 416 | + | fn compute_bandwidth( | |
| 417 | + | elapsed_seconds: f64, | |
| 418 | + | iterations: u32, | |
| 419 | + | timing: TimingSource, | |
| 420 | + | ) -> BandwidthResult { | |
| 338 | 421 | let bytes_per_iter_read = 2.0 * (BUFFER_BYTES as f64); | |
| 339 | 422 | let bytes_per_iter_write = BUFFER_BYTES as f64; | |
| 340 | 423 | let iters = f64::from(iterations); | |
| 341 | 424 | BandwidthResult { | |
| 342 | 425 | read_bps: bytes_per_iter_read * iters / elapsed_seconds, | |
| 343 | 426 | write_bps: bytes_per_iter_write * iters / elapsed_seconds, | |
| 427 | + | timing, | |
| 344 | 428 | } | |
| 345 | 429 | } |
| @@ -10,6 +10,6 @@ | |||
| 10 | 10 | pub mod launch; | |
| 11 | 11 | pub mod vulkan_ctx; | |
| 12 | 12 | ||
| 13 | - | pub use bandwidth::{BandwidthError, BandwidthResult, measure_bandwidth}; | |
| 13 | + | pub use bandwidth::{BandwidthError, BandwidthResult, TimingSource, measure_bandwidth}; | |
| 14 | 14 | pub use launch::{LaunchError, LaunchOverheadResult, measure_launch_overhead}; | |
| 15 | 15 | pub use vulkan_ctx::{VulkanContext, VulkanContextError}; |
| @@ -44,6 +44,13 @@ | |||
| 44 | 44 | pub physical_device: vk::PhysicalDevice, | |
| 45 | 45 | pub memory_properties: vk::PhysicalDeviceMemoryProperties, | |
| 46 | 46 | ||
| 47 | + | /// Nanoseconds per timestamp-query tick on this device. | |
| 48 | + | pub timestamp_period_ns: f32, | |
| 49 | + | /// Meaningful low-order bits in a timestamp query on the chosen | |
| 50 | + | /// queue family. Zero means the queue cannot write timestamps, and | |
| 51 | + | /// probes must fall back to host timing. | |
| 52 | + | pub timestamp_valid_bits: u32, | |
| 53 | + | ||
| 47 | 54 | // Held for cleanup. Dropped in reverse declaration order. | |
| 48 | 55 | pub device: ash::Device, | |
| 49 | 56 | pub instance: Instance, | |
| @@ -63,6 +70,14 @@ | |||
| 63 | 70 | ||
| 64 | 71 | let queue_family_index = pick_compute_queue_family(&instance, physical)?; | |
| 65 | 72 | ||
| 73 | + | let timestamp_period_ns = unsafe { instance.get_physical_device_properties(physical) } | |
| 74 | + | .limits | |
| 75 | + | .timestamp_period; | |
| 76 | + | let timestamp_valid_bits = | |
| 77 | + | unsafe { instance.get_physical_device_queue_family_properties(physical) } | |
| 78 | + | .get(queue_family_index as usize) | |
| 79 | + | .map_or(0, |fam| fam.timestamp_valid_bits); | |
| 80 | + | ||
| 66 | 81 | let priorities = [1.0_f32]; | |
| 67 | 82 | let queue_info = vk::DeviceQueueCreateInfo::default() | |
| 68 | 83 | .queue_family_index(queue_family_index) | |
| @@ -86,6 +101,8 @@ | |||
| 86 | 101 | command_pool, | |
| 87 | 102 | physical_device: physical, | |
| 88 | 103 | memory_properties, | |
| 104 | + | timestamp_period_ns, | |
| 105 | + | timestamp_valid_bits, | |
| 89 | 106 | device, | |
| 90 | 107 | instance, | |
| 91 | 108 | _entry: entry, |