Source browser

examples/real_data_support.hpp

Read the complete file below or download its original bytes. Line numbers provide stable links to a declaration or processing step.

File fingerprint
1/** @file real_data_support.hpp
2 * @brief Portable fixture readers and complete CPU/GPU output comparisons.
3 * Dataset paths contain data only. Shader sources remain embedded in the executable.
4 */
5#ifndef SHDR_EXAMPLES_REAL_DATA_SUPPORT_HPP // Include fixture support only once.
6#define SHDR_EXAMPLES_REAL_DATA_SUPPORT_HPP // Mark the support declarations as available.
7#include "runner.hpp" // Select the same execution backend as the other examples.
8#include <cstdint> // Assemble endian-defined dataset words explicitly.
9#include <cstdlib> // Allow a caller to choose a dataset directory through the environment.
10#include <cstring> // Copy float representations with std::memcpy.
11#include <fstream> // Read ordinary binary datasets from disk.
12#include <functional> // Restore mutable resources before comparing independent executions.
13#include <limits> // Check that float fixtures have the documented representation.
14
15namespace shdr_examples { // Keep fixture utilities outside the public library API.
16namespace real_data { // Group the real-data example's input and comparison helpers.
17inline std::string directory() { // Resolve the supplied fixture directory at runtime.
18    const char* override_path = std::getenv("SHDR_DATA_DIR"); // Check an optional caller override.
19    if (override_path && *override_path) { // Accept a nonempty explicit dataset location.
20        return override_path; // Use only this directory for the real-data fixture reads.
21    } // Ordinary builds use the CMake-provided source fixture directory.
22#ifdef SHDR_EXAMPLE_DATA_DIR // Select the dataset directory recorded by the example build.
23    return SHDR_EXAMPLE_DATA_DIR; // Return the ordinary example fixture path.
24#else // Keep a useful relative fallback for manual builds.
25    return "examples/data"; // Resolve fixtures relative to the caller's working directory.
26#endif // Finish the build-path selection.
27} // Use this selected directory to resolve prepared dataset filenames.
28inline std::vector<unsigned char> // Read one bounded fixture file.
29bytes(const std::string& name, // Read one bounded fixture file.
30      const std::string& base = directory()) { // Read one bounded fixture file.
31    const std::string path = base + "/" + name; // Join the fixed fixture filename to its directory.
32    std::ifstream input(path.c_str(), // Open at the end to measure size.
33                        std::ios::binary | std::ios::ate); // Open at the end to measure size.
34    if (!input) { // Identify a missing prepared dataset immediately.
35        throw shdr::error("cannot open dataset " + path + // Report the failed format, numerical or resource precondition.
36                          ", run scripts/prepare_datasets.py first"); // Explain preparation.
37    } // The dataset path is now open as an ordinary binary stream.
38    const std::streamoff count = input.tellg(); // Measure the exact byte count before allocation.
39    if (count <= 0 || // Reject empty or unexpectedly large example inputs.
40        count > 64 * 1024 * 1024) { // Reject empty or unexpectedly large example inputs.
41        throw shdr::error("invalid example dataset size: " + // Report the failed format, numerical or resource precondition.
42                          name); // Keep fixture allocations bounded.
43    } // All documented public fixtures fit within this simple limit.
44    std::vector<unsigned char> result( // Allocate the measured payload.
45        static_cast<std::size_t>(count)); // Allocate the measured payload.
46    input.seekg(0); // Return to the first dataset byte.
47    input.read(reinterpret_cast<char*>(result.data()), count); // Read the complete fixture payload.
48    if (!input) { // Catch incomplete file reads before interpreting any value.
49        throw shdr::error("dataset read was incomplete: " + // Report the failed format, numerical or resource precondition.
50                          name); // Report truncation or an I/O failure.
51    } // The caller receives a fully read binary fixture.
52    return result; // Transfer the owned byte array to its decoder.
53} // Preserve the file order of the raw byte values.
54inline std::vector<float> // Decode little-endian IEEE float32 fixtures.
55floats(const std::string& name) { // Decode little-endian IEEE float32 fixtures.
56    static_assert(sizeof(float) == 4 && std::numeric_limits<float>::is_iec559, // Check representation.
57                  "float32 fixtures require IEEE floats"); // Check representation.
58    const std::vector<unsigned char> raw = bytes(name); // Read the complete named float fixture.
59    if (raw.size() % 4u) { // Require four bytes for every declared float32 value.
60        throw shdr::error("float dataset contains a partial value: " + // Report the failed format, numerical or resource precondition.
61                          name); // Reject incomplete representations.
62    } // Whole values can now be assembled independently of endian order.
63    std::vector<float> result(raw.size() / 4u); // Allocate one native float per encoded value.
64    for (std::size_t i = 0; i < result.size(); ++i) { // Decode every payload element.
65        const std::uint32_t bits = // Assemble bytes in documented order.
66            std::uint32_t(raw[4u * i]) | (std::uint32_t(raw[4u * i + 1u]) << 8u) | // Assemble bytes in documented order.
67            (std::uint32_t(raw[4u * i + 2u]) << 16u) | // Assemble bytes in documented order.
68            (std::uint32_t(raw[4u * i + 3u]) << 24u); // Assemble bytes in documented order.
69        std::memcpy( // Copy the assembled float32 representation into its float value.
70            &result[i], &bits, sizeof(float)); // Copy the assembled float32 representation into its float value.
71        if (!std::isfinite(result[i])) { // Keep dataset corruption out of numerical comparisons.
72            throw shdr::error("nonfinite float in dataset: " + // Report the failed format, numerical or resource precondition.
73                              name); // Reject invalid signal or model values.
74        } // Valid values retain their published float32 bit patterns.
75    } // The full fixture is now usable by either backend.
76    return result; // Supply the decoded scalar array.
77} // The example supports both little-endian and big-endian hosts.
78inline std::vector<std::uint16_t> // Decode EVT3 little-endian words.
79words(const std::string& name) { // Decode EVT3 little-endian words.
80    const std::vector<unsigned char> raw = // Read the public recording's prepared prefix.
81        bytes(name); // Read the public recording's prepared prefix.
82    if (raw.size() % 2u) { // Require complete sensor words.
83        throw shdr::error("EVT3 dataset contains a partial word"); // Report a truncated fixture.
84    } // Each pair of bytes forms one sensor word.
85    std::vector<std::uint16_t> result(raw.size() / 2u); // Allocate the word-oriented decoder input.
86    for (std::size_t i = 0; i < result.size(); ++i) { // Convert every complete word.
87        result[i] = static_cast<std::uint16_t>( // Decode low byte first.
88            unsigned(raw[2u * i]) | (unsigned(raw[2u * i + 1u]) << 8u)); // Decode low byte first.
89    } // Assemble each little-endian word from its two stored bytes.
90    return result; // Return the original sensor word sequence.
91} // The RAW text header is removed only by the documented preparation script.
92template <class T> // Apply the same helper to each compatible scalar or vector resource type.
93inline shdr::buffer<T> resource(const std::vector<T>& values) { // Upload ordinary prepared arrays.
94    shdr::buffer<T> result(values.size()); // Allocate a typed resource of the exact element count.
95    std::copy(values.begin(), // Copy the preserved source values into their input resource.
96              values.end(), // Copy the dataset into host resource storage.
97              result.data()); // Copy the dataset into host resource storage.
98    return result; // Share the resulting resource with pipeline stages.
99} // GPU upload remains deferred until a device actually uses the resource.
100inline double difference(float actual, // Measure a finite scalar output difference.
101                         float expected) { // Measure a finite scalar output difference.
102    if (!std::isfinite(actual) || // Reject numerical failures on either backend.
103        !std::isfinite(expected)) { // Reject numerical failures on either backend.
104        throw shdr::error( // Report the failed format, numerical or resource precondition.
105            "real-data pipeline produced a nonfinite output"); // Name an invalid processing result.
106    } // Relative scaling makes the comparison meaningful for logits and signal amplitudes.
107    return std::fabs(double(actual) - expected) / // Combine absolute and relative error.
108           (1.0 + std::fabs(double(expected))); // Combine absolute and relative error.
109} // A tolerance of zero requires bit-identical numerical values.
110inline double difference(unsigned actual, unsigned expected) { // Compare discrete outputs exactly.
111    return actual == expected ? 0.0 : 1.0; // Keep event words, masks and lengths lossless.
112} // Require exact equality for integer outputs.
113template <class T, std::size_t N> // Apply the same helper to each compatible scalar or vector resource type.
114inline double difference(const glsl::vec<T, N>& actual, // Compare every vector component.
115                         const glsl::vec<T, N>& expected) { // Compare every vector component.
116    double maximum = 0.0; // Track the worst normalized component difference.
117    for (std::size_t component = 0; component < N; // Include both coordinates and metadata components.
118         ++component) { // Include both coordinates and metadata components.
119        maximum = std::max(maximum, // Apply the matching scalar policy.
120                           difference(actual[component], // Apply the matching scalar policy.
121                                      expected[component])); // Apply the matching scalar policy.
122    } // Vectors use their strictest component result.
123    return maximum; // Return the complete element's comparison error.
124} // Compare the typed vector components through the normalized scalar-error helper.
125class output_check { // Provide a common interface for several typed stage outputs.
126  public: // Expose only the capture and comparison operations needed by the evaluator.
127    virtual ~output_check() { // Destroy a concrete checker through this base type safely.
128    } // Destroy a concrete checker through this base type safely.
129    virtual void capture() = 0; // Save completed CPU output as the backend comparison reference.
130    virtual double verify() const = 0; // Compare the requested backend against that saved output.
131}; // A pipeline can validate several differently typed resources together.
132template <class T> // Apply the same helper to each compatible scalar or vector resource type.
133class checked_output : public output_check { // Keep an output resource and its CPU snapshot.
134    const shdr::buffer<T>& resource_; // Read completed results through a constant resource reference.
135    std::vector<T> reference_; // Preserve a complete CPU result across GPU execution.
136    double tolerance_; // Record the permitted normalized floating-point difference.
137    std::size_t // Compare only the meaningful output prefix when storage has spare capacity.
138        count_; // Compare only the meaningful output prefix when storage has spare capacity.
139  public: // Let each test choose the resource, error limit and optional compact count.
140    explicit checked_output(const shdr::buffer<T>& value, // Configure the output comparison.
141                            double tolerance = 0.0001, // Configure the output comparison.
142                            std::size_t count = 0) // Configure the output comparison.
143        : resource_(value), tolerance_(tolerance), // Configure the output comparison.
144          count_(count ? count : value.size()) { // Configure the output comparison.
145        if (count_ > // Prevent a checker from reading beyond allocated resource storage.
146            value.size()) { // Prevent a checker from reading beyond allocated resource storage.
147            throw shdr::error( // Report the failed format, numerical or resource precondition.
148                "real-data comparison count exceeds resource storage"); // Explain the invalid check.
149        } // The meaningful output range is now bounded.
150    } // The reference is populated only after CPU processing.
151    void capture() override { // Save a complete finite CPU output reference.
152        const T* values = resource_.data(); // Obtain completed host-readable resource values.
153        reference_.assign(values, values + count_); // Keep all meaningful output elements.
154        for (const T& value : // Validate CPU numerical results before using them as a reference.
155             reference_) { // Validate CPU numerical results before using them as a reference.
156            (void)difference(value, value); // Self-comparison detects nonfinite float components.
157        } // Discrete values remain exact in the snapshot.
158    } // Keep an independent CPU reference for later GPU comparisons.
159    double verify() const override { // Compare every completed output value with its CPU reference.
160        const T* values = // Read back GPU results only after the pipeline has been submitted.
161            resource_.data(); // Read back GPU results only after the pipeline has been submitted.
162        double maximum = 0.0; // Track a useful measured backend discrepancy.
163        for (std::size_t i = 0; i < reference_.size(); ++i) { // Check the full meaningful result.
164            maximum = std::max( // Compare the appropriate value type.
165                maximum, // Compare the appropriate value type.
166                difference(values[i], reference_[i])); // Compare the appropriate value type.
167            if (maximum > // Fail as soon as a result exceeds its explicit error limit.
168                tolerance_) { // Fail as soon as a result exceeds its explicit error limit.
169                throw shdr::error("real-data CPU/GPU output differs at element " + // Report the failed format, numerical or resource precondition.
170                                  std::to_string(i)); // Identify the bad location.
171            } // Successful outputs stay within the documented comparison tolerance.
172        } // All elements have now been compared.
173        return maximum; // Report the worst measured normalized difference.
174    } // Finish comparing completed results with the stored CPU reference.
175}; // Own the CPU reference vector and borrow the pipeline resource.
176inline void // Execute identical real inputs on both backends.
177evaluate(runner& run, // Execute identical real inputs on both backends.
178         const std::string& label, // Execute identical real inputs on both backends.
179         const shdr::pipeline& pipeline, // Execute identical real inputs on both backends.
180         std::initializer_list<output_check*> outputs, // Execute identical real inputs on both backends.
181         const std::function<void()>& reset = // Execute identical real inputs on both backends.
182             std::function<void()>()) { // Execute identical real inputs on both backends.
183    if (reset) { // Restore trainable parameters or streaming state when a case needs it.
184        reset(); // Establish the CPU run's documented starting state.
185    } // Stateless cases use their existing immutable input resources.
186    pipeline.run(); // Compute the complete CPU reference from real dataset values.
187    for (output_check* output : outputs) { // Capture every selected intermediate and final result.
188        output->capture(); // Preserve completed output before a device may overwrite it.
189    } // The comparison snapshots now describe this exact pipeline invocation.
190    if (run.gpu()) { // Run the requested profile only when the user selected a GPU backend.
191        if (reset) { // Give GPU execution the same initial mutable resources as CPU execution.
192            reset(); // Restore parameters or state before replaying the pipeline.
193        } // Input datasets remain identical across both runs.
194        run.run(pipeline); // Submit the linked stages through the selected library device.
195    } // CPU-only runs still validate finite and exact output checks below.
196    double maximum = 0.0; // Track the worst error across all selected stage resources.
197    for (const output_check* output : outputs) { // Validate the full result for each typed output.
198        maximum = // Include every recorded stage in the comparison.
199            std::max(maximum, output->verify()); // Include every recorded stage in the comparison.
200    } // Backend errors become ordinary test failures with useful element diagnostics.
201    std::cout << label << ": verified, normalized CPU/GPU error " << maximum // Report the completed real-data processing check.
202              << '\n'; // Report a measured functional check.
203} // Timed randomized benchmarks remain separate from these correctness runs.
204} // namespace real_data
205} // namespace shdr_examples
206#endif // Finish the fixture support include guard.