Skip to content

Commit e675159

Browse files
committed
added comments
1 parent a1acce9 commit e675159

6 files changed

Lines changed: 941 additions & 45 deletions

backends/ze/btx_zeinterval_callbacks.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,10 +1031,10 @@ static void event_profiling_result_callback(void *btx_handle,
10311031
device_hash = hash_device(it0->second);
10321032
}
10331033
uintptr_t subdevice_hash = 0;
1034-
const auto it1 = data->subdevice_parent.find({hostname, vpid, (thapi_device_id)device});
1034+
const auto it1 = data->subdevice_parent.find(std::make_tuple({hostname, vpid, (thapi_device_id)device}));
10351035
if (it1 != data->subdevice_parent.cend()) {
10361036
subdevice_hash = device_hash;
1037-
const auto it2 = data->device_property.find({hostname, vpid, it1->second});
1037+
const auto it2 = data->device_property.find(std::make_tuple({hostname, vpid, it1->second}));
10381038
if (it2 != data->device_property.cend())
10391039
subdevice_hash = hash_device(it2->second);
10401040
}

backends/ze/ze_validator.in

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,99 @@
11
#!/usr/bin/env ruby
22
# coding: utf-8
3+
# =============================================================================
4+
# ze_validator -- a correctness checker for Intel Level Zero ("ze") programs.
5+
#
6+
# WHAT THIS IS
7+
# ------------
8+
# THAPI (Tracing Heterogeneous APIs) ships a tracer that intercepts every call
9+
# an application makes into the Level Zero runtime and writes it to an LTTng
10+
# trace on disk (a "CTF" trace). This script reads such a trace back and
11+
# replays it against a software model of what the Level Zero runtime would have
12+
# been doing, looking for API misuse the runtime itself does not diagnose:
13+
# leaks, use-after-free, deadlocks, cross-context handle mixing, and so on.
14+
#
15+
# It never runs on the GPU and never touches the application. It is a pure
16+
# post-mortem analysis of a recorded trace.
17+
#
18+
# THE FIVE FILES THAT MAKE UP THE VALIDATOR
19+
# -----------------------------------------
20+
# ze_validator.in <-- YOU ARE HERE
21+
# Executable entry point. Parses CLI options, wires up the babeltrace2
22+
# graph that decodes the trace, and hands each decoded event to the
23+
# StateObject. ".in" means autoconf preprocesses it at build time,
24+
# substituting @prefix@ below; the installed file is named `ze_validator`.
25+
#
26+
# ze_validator_zemodel.rb
27+
# Pure data model. Plain Ruby classes mirroring Level Zero objects
28+
# (Device, Context, CommandList, Event, Fence, Memory, ...) plus the
29+
# bookkeeping types used for deferred execution (RecordedOp, DeferredUnit).
30+
# No checking logic lives here -- just state.
31+
#
32+
# ze_validator_state_object.rb
33+
# The engine. Holds the whole model (per host / per process / per thread),
34+
# drives the trace-consumption loop, runs the deferred-execution
35+
# scheduler, and owns every error-reporting method (print_usage_error etc).
36+
#
37+
# ze_validator_function_entry_exit_callbacks.rb
38+
# The dispatch tables. Three global hashes mapping a ze API name to a
39+
# lambda: $upon_entry, $on_successful_exit, $on_erroneous_exit. This is
40+
# where "what does zeMemAllocDevice do to the model" is written.
41+
#
42+
# ze_validator_entry_exit_helpers.rb
43+
# The check library. The `check_*` functions those callbacks call --
44+
# out-of-bounds copies, use-after-free, deadlock cycles, context matching.
45+
#
46+
# HOW A TRACE BECOMES A DIAGNOSTIC (the data flow)
47+
# ------------------------------------------------
48+
# trace on disk
49+
# -> babeltrace2 graph (built in build_and_run_graph below)
50+
# -> StateObject#consume receives one CTF event at a time
51+
# -> event name is matched against /:(z.*)_(entry|exit)/ so
52+
# "lttng_ust_ze:zeMemAllocDevice_entry" yields api="zeMemAllocDevice",
53+
# phase="entry"
54+
# -> StateObject#on_entry / #on_exit look the API up in the dispatch tables
55+
# -> the callback mutates the model and/or calls a check_* helper
56+
# -> a violated check prints "Level Zero <Kind> Error: ..." to stderr
57+
# -> at end of trace StateObject#check_issues reports leaks and deadlocks
58+
#
59+
# WHY EVERY API APPEARS TWICE (entry and exit)
60+
# --------------------------------------------
61+
# The tracer emits one event just BEFORE the call enters the driver (_entry,
62+
# carrying the input arguments) and one just AFTER it returns (_exit, carrying
63+
# the return code and any output pointers). This matters constantly in the
64+
# callback code:
65+
# * Output handles (the thing a Create call produced) only exist at _exit.
66+
# * If the driver CRASHES inside the call, the _exit event is never written --
67+
# so any check that could be the thing that crashes must run at _entry, or
68+
# it will silently never fire. Several callbacks carry a comment saying
69+
# exactly this.
70+
#
71+
# TYPICAL USE
72+
# -----------
73+
# iprof -t -- ./my_app # record a trace with the THAPI tracer
74+
# ze_validator ~/lttng-traces/... # replay it through this validator
75+
# =============================================================================
76+
77+
# Where the installed data files live (ze_thread_safety.yaml, ze_deprecated.json,
78+
# ze_device_property.json) and where the ze_library Ruby bindings are found.
79+
# @prefix@ is replaced by autoconf with the configure --prefix at build time.
380
DATADIR = File.join("@prefix@", "share")
481
BINDIR = File.join("@prefix@", "bin")
582
$:.unshift(DATADIR) if File.directory?(DATADIR)
683
require 'optparse'
7-
require 'babeltrace2'
84+
require 'babeltrace2' # Ruby bindings for babeltrace2, the CTF trace reader
885
require 'find'
9-
require 'ze_library'
86+
require 'ze_library' # generated FFI bindings: ZE::ZEResult, ZE::ZE*Desc structs
1087
require 'pp'
1188
require 'set'
12-
require 'ze_validator_zemodel'
13-
require 'ze_validator_function_entry_exit_callbacks'
14-
require 'ze_validator_state_object'
89+
require 'ze_validator_zemodel' # the object model (ZEModel::*)
90+
require 'ze_validator_function_entry_exit_callbacks' # populates $upon_entry / $on_*_exit
91+
require 'ze_validator_state_object' # the StateObject engine
1592
require 'yaml'
1693

1794
# Don't complain about broken pipe
95+
# (restores the default kill-on-SIGPIPE so piping our output into e.g. `head`
96+
# terminates quietly instead of raising Errno::EPIPE out of a puts)
1897
Signal.trap('SIGPIPE', 'SYSTEM_DEFAULT')
1998

2099
# Runs the ze_device_property helper binary to (re)generate ze_device_property.json
@@ -36,6 +115,12 @@ rescue SystemCallError => e
36115
"continuing without device properties."
37116
end
38117

118+
# Command-line defaults. All checking categories are ON unless explicitly
119+
# disabled, so a plain `ze_validator <trace>` gives the most thorough report.
120+
# live - read a live LTTng session instead of a trace directory
121+
# device_agnostic - report portability hazards (hardcoded ordinals etc.)
122+
# performance - report API usage that costs performance
123+
# gen_device_properties - shell out to the ze_device_property helper first
39124
$options = { live: false, device_agnostic: true, performance: true,
40125
gen_device_properties: true }
41126

@@ -66,15 +151,46 @@ OptionParser.new do |opts|
66151
end.parse!
67152

68153

154+
# Builds and runs the babeltrace2 processing graph that decodes the trace and
155+
# feeds it to the validator, then triggers the end-of-trace reporting pass.
156+
#
157+
# babeltrace2 works as a dataflow graph of components connected port-to-port:
158+
#
159+
# [source: trace_0] --\
160+
# [source: trace_1] ----> [filter: muxer] --> [sink: our StateObject]
161+
# [source: trace_2] --/
162+
#
163+
# * SOURCES decode one CTF trace directory each into a stream of messages.
164+
# A traced run produces one trace per process (and per node on a cluster),
165+
# hence potentially many sources.
166+
# * The MUXER merges those streams into a single stream ordered by timestamp.
167+
# This is essential: the validator's model assumes it sees events in the
168+
# order they really happened, across all processes and threads.
169+
# * The SINK is us -- sink_object.consume is a lambda invoked with each batch
170+
# of messages (see StateObject#consume).
171+
#
172+
# `source_location` is the list of paths given on the command line.
69173
def build_and_run_graph( source_location, sink_object )
70174
# build graph and set up source
71175
graph = BT2::BTGraph.new
72176

177+
# Look up the three component classes we need from babeltrace2's plugins.
178+
# ctf.fs - read a CTF trace from the filesystem
179+
# ctf.lttng-live - attach to a running LTTng session over the network
180+
# utils.muxer - timestamp-order-merge several streams into one
73181
ctf_fs = BT2::BTPlugin.find('ctf').get_source_component_class_by_name('fs')
74182
ctf_lttng_live = BT2::BTPlugin.find("ctf").get_source_component_class_by_name("lttng-live")
75183
utils_muxer = BT2::BTPlugin.find('utils').get_filter_component_class_by_name('muxer')
76184

77185
if !$options[:live]
186+
# Offline mode: the user hands us a directory that may contain many traces
187+
# nested at arbitrary depth (LTTng lays out one subdirectory per process,
188+
# per node, per UST channel). Walk it and work out which directories are
189+
# actually readable CTF traces, in four steps:
190+
# 1. recursively enumerate every path, keeping only files
191+
# 2. keep the ones literally named "metadata" -- every CTF trace directory
192+
# contains exactly one such file describing its event layout
193+
# 3. take that file's parent directory: that IS the trace directory
78194
trace_locations =
79195
Find.find(*source_location).reject do |path|
80196
FileTest.directory?(path)
@@ -88,10 +204,16 @@ def build_and_run_graph( source_location, sink_object )
88204
qe.query.value['weight'] > 0.5
89205
end
90206
else
207+
# Live mode: the arguments are LTTng relay-daemon URLs, not paths, so there
208+
# is nothing on disk to search -- pass them through untouched.
91209
trace_locations = source_location
92210
end
93211
raise 'Could not find lttng trace' if trace_locations.size == 0
94212

213+
# Add one source component per trace. Each gets a unique name ("trace_0",
214+
# "trace_1", ...) because babeltrace requires component names to be distinct.
215+
# In live mode, "session-not-found-action" => "end" makes the component finish
216+
# cleanly rather than hang forever when the named session does not exist.
95217
if !$options[:live]
96218
comp_sources = trace_locations.each_with_index.collect { |trace_location, i| graph.add_component(ctf_fs, "trace_#{i}", params: {"inputs" => [ trace_location ] }) }
97219
else
@@ -101,32 +223,59 @@ def build_and_run_graph( source_location, sink_object )
101223
# Muxer
102224
comp_muxer = graph.add_component(utils_muxer, 'mux')
103225

226+
# The sink is where our code plugs in: `sink_object.consume` returns a lambda
227+
# that babeltrace calls with an iterator over each batch of decoded messages.
228+
# That lambda is the top of the whole validation pipeline.
104229
sink = graph.add_simple_sink('babeltrace_thapi', sink_object.consume)
105230

106231
# Sources to muxer
232+
# A single trace may expose several output ports (one per CTF stream, i.e.
233+
# roughly per traced thread), so flat_map over all sources' ports and wire
234+
# port i of the collected list into muxer input port i. The muxer grows its
235+
# input ports on demand, which is why we can index it freely.
107236
comp_sources.flat_map(&:output_ports).each_with_index do |op, i|
108237
ip = comp_muxer.input_port(i)
109238
graph.connect_ports(op, ip)
110239
end
111240

112241
# Chain the rest
242+
# Only one link is left (muxer -> sink). It is written as a generic
243+
# each_cons(2) pairwise chain so extra filter components could be spliced into
244+
# the array without changing this code.
113245
[comp_muxer, sink].flatten.each_cons(2) do |_out, _in|
114246
op = _out.output_port(0)
115247
ip = _in.input_port(0)
116248
graph.connect_ports(op, ip)
117249
end
118250

251+
# Pull the whole trace through the graph. This call does not return until
252+
# every event has been consumed, so by the time it finishes the model
253+
# reflects the entire run.
119254
graph.run
255+
# End-of-trace pass: drain any still-pending deferred command lists, report
256+
# deadlocks, and report every object that was created but never destroyed.
257+
# Errors found DURING the run were already printed as they were discovered;
258+
# these are the ones only visible once you know nothing more is coming.
120259
sink_object.check_issues()
121260

122261
end
123262

124263
# only executive this code if we launch this as the main
125264
# script. if it's just included with "require" we just want access to the functions.
265+
# (Ruby's equivalent of C's `int main` guard: $0 is the script that was invoked,
266+
# __FILE__ is this file, so they match only when run directly. Tests can then
267+
# `require` this file to reuse build_and_run_graph without starting an analysis.)
126268
if __FILE__ == $0
269+
# Refresh ze_device_property.json first, so StateObject picks up the real
270+
# command-queue-group topology of this machine when it is constructed below.
127271
generate_device_properties if $options[:gen_device_properties]
272+
# One StateObject holds the entire model for the whole run and acts as the
273+
# graph's sink.
128274
sink_obj = StateObject.new(device_agnostic: $options[:device_agnostic],
129275
performance: $options[:performance])
276+
# Whatever is left in ARGV after OptionParser#parse! are the trace paths.
277+
# Deduplicate: feeding the same trace twice would double-apply every event and
278+
# corrupt the model (e.g. a second create for an already-live handle).
130279
ARGV.uniq!
131280
source_location = ARGV
132281
build_and_run_graph(source_location, sink_obj)

0 commit comments

Comments
 (0)