FZGPUModules 2.0
GPU-accelerated modular compression pipelines
Loading...
Searching...
No Matches
compressor.h
Go to the documentation of this file.
1
5#pragma once
6
7#include "backend/types.h"
8#include "advanced/dag.h"
10#include "pipeline/perf.h"
11#include "pipeline/config.h"
12#include "stage/stage.h"
13#include "stage/stage_factory.h"
14#include "mem/mempool.h"
15#include "fzm_format.h"
16
17#include <array>
18#include <memory>
19#include <stdexcept>
20#include <string>
21#include <unordered_map>
22#include <vector>
23
24namespace fz {
25
48enum class FusionPolicy { Off, Auto, Force };
49
52 std::string implementation;
53 std::vector<std::string> stages;
54};
55
57struct FusionInfo {
58 FusionPolicy policy = FusionPolicy::Off;
59 size_t legal_group_count = 0;
60 std::vector<FusionGroupInfo> installed_groups;
62 std::string fallback_reason;
63};
64
65class Pipeline {
66public:
72 explicit Pipeline(
73 size_t input_data_size = 0,
75 float pool_multiplier = 3.0f
76 );
77
85 explicit Pipeline(const std::string& config_path);
86
87 ~Pipeline();
88
89 // ── Configuration ─────────────────────────────────────────────────────────
90
92 void setMemoryStrategy(MemoryStrategy strategy);
93
95 void setFusionPolicy(FusionPolicy mode) { fusion_policy_ = mode; }
96 FusionPolicy getFusionPolicy() const { return fusion_policy_; }
98 size_t getFusedGroupCount() const { return dag_ ? dag_->getFusedGroupCount() : 0; }
100 const FusionInfo& getFusionInfo() const { return fusion_info_; }
101
103 void setNumStreams(int num_streams);
104
111 void setDims(size_t x, size_t y = 1, size_t z = 1) { dims_ = {x, y, z}; }
112 void setDims(std::array<size_t, 3> dims) { dims_ = dims; }
113 std::array<size_t, 3> getDims() const { return dims_; }
114
115 // ── Builder API ───────────────────────────────────────────────────────────
116
121 template<typename StageT, typename... Args>
122 StageT* addStage(Args&&... args);
123
131 int connect(Stage* dependent, Stage* producer, const std::string& output_name = "output");
132
134 int connect(Stage* dependent, const std::vector<Stage*>& producers);
135
159 void bindExternalInput(Stage* stage);
160
166 void finalize();
167
173 void warmup(fz::stream_t stream = 0);
174
176 void setWarmupOnFinalize(bool enable) { warmup_on_finalize_ = enable; }
177 bool isWarmupOnFinalizeEnabled() const { return warmup_on_finalize_; }
178
185 void setPoolManagedDecompOutput(bool enable) { pool_managed_decomp_ = enable; }
186 bool isPoolManagedDecompOutput() const { return pool_managed_decomp_; }
187
220 void setPrimarySource(Stage* stage) { primary_source_stage_ = stage; }
221
235 size_t getMaxCompressedSize(size_t input_bytes) const;
236
256 size_t getLastUncompressedSize() const {
257 return original_input_size_ > 0 ? original_input_size_ : input_size_;
258 }
259
260 // ── Execution ─────────────────────────────────────────────────────────────
261
273 void compress(
274 const void* d_input,
275 size_t input_size,
276 void** d_output,
277 size_t* output_size,
278 fz::stream_t stream = 0
279 );
280
307 void compress(
308 const void* d_input,
309 size_t input_size,
310 void* d_output_buf,
311 size_t output_buf_capacity,
312 size_t* actual_output_size,
313 fz::stream_t stream = 0
314 );
315
330 void decompress(
331 const void* d_input,
332 size_t input_size,
333 void** d_output,
334 size_t* output_size,
335 fz::stream_t stream = 0
336 );
337
358 void decompress(
359 const void* d_input,
360 size_t input_size,
361 void* d_output_buf,
362 size_t output_buf_capacity,
363 size_t* actual_output_size,
364 fz::stream_t stream = 0
365 );
366
404 void decompressInto(
405 const void* d_input,
406 size_t input_size,
407 void* d_output_buf,
408 size_t output_buf_capacity,
409 size_t* actual_output_size,
410 fz::stream_t stream = 0
411 );
412
413 // ── Explicit-ownership execution API ──────────────────────────────────────
414 //
415 // Span-based wrappers over the pointer overloads above. Behavior is
416 // identical — these exist so ownership is visible in the signature instead
417 // of depending on `void**` vs `void*` and on setPoolManagedDecompOutput().
418 // Prefer these in new code; the pointer overloads remain supported.
419
425 BorrowedDeviceBuffer compress(ConstDeviceSpan input, fz::stream_t stream = 0);
426
432 size_t compressInto(ConstDeviceSpan input, DeviceSpan output, fz::stream_t stream = 0);
433
442 BorrowedDeviceBuffer decompressBorrowed(ConstDeviceSpan input, fz::stream_t stream = 0);
443
449 OwnedDeviceBuffer decompressOwned(ConstDeviceSpan input, fz::stream_t stream = 0);
450
456 size_t decompressInto(ConstDeviceSpan input, DeviceSpan output, fz::stream_t stream = 0);
457
464 size_t decompressIntoAsync(ConstDeviceSpan input, DeviceSpan output, fz::stream_t stream = 0);
465
490 void prepareInverse(size_t uncompressed_size);
491
493 void reset(fz::stream_t stream = 0);
494
495 // ── Profiling ─────────────────────────────────────────────────────────────
496
501 void enableProfiling(bool enable);
502 bool isProfilingEnabled() const { return profiling_enabled_; }
503
505 const PipelinePerfResult& getLastPerfResult() const { return last_perf_result_; }
506
508 CompressionDAG* getDAG() { return dag_.get(); }
509
511 size_t getPoolThreshold() const;
512
522 bool isMemPoolFallbackMode() const;
523
529 void enableBoundsCheck(bool enable) { dag_->enableBoundsCheck(enable); }
530 bool isBoundsCheckEnabled() const { return dag_->isBoundsCheckEnabled(); }
531
537 void setColoringEnabled(bool enable) {
538 coloring_enabled_ = enable; // survives a setMemoryStrategy() DAG swap
539 dag_->setColoringEnabled(enable);
540 }
541 bool isColoringEnabled() const { return dag_->isColoringEnabled(); }
542 size_t getColorRegionCount() const { return dag_->getColorRegionCount(); }
543
551 bool isColoringRequested() const { return coloring_enabled_; }
552
562 std::unordered_map<std::string, std::vector<std::string>> collectRunNotes() const {
563 std::unordered_map<std::string, std::vector<std::string>> notes;
564 for (const auto& s : stages_) {
565 if (!s) continue;
566 auto n = s->getRunNotes();
567 if (!n.empty()) notes.emplace(s->getName(), std::move(n));
568 }
569 return notes;
570 }
571
572 // ── CUDA Graph Capture (compression-only) ─────────────────────────────────
573
581 void enableGraphMode(bool enable);
582 bool isGraphModeEnabled() const { return graph_mode_enabled_; }
583
594 void captureGraph(fz::stream_t stream = 0);
595 bool isGraphCaptured() const { return graph_captured_; }
596
597 size_t getPeakMemoryUsage() const;
598 size_t getCurrentMemoryUsage() const;
599 void printPipeline() const;
600
601 // ── File Serialization ────────────────────────────────────────────────────
602
605 FZMHeaderCore core;
606 std::vector<FZMStageInfo> stages;
607 std::vector<FZMBufferEntry> buffers;
608 };
609
611 void writeToFile(const std::string& filename, fz::stream_t stream = 0);
612
614 static FZMFileHeader readHeader(const std::string& filename);
615
617 FZMFileHeader buildHeader() const;
618
619 // ── In-memory metadata header (decode without a prior compress) ────────────
620
635 std::vector<uint8_t> serializeHeaderToMemory() const;
636
656 void primeInverseFromHeader(const void* header_bytes, size_t header_size);
657
672 static void decompressFromFile(
673 const std::string& filename,
674 void** d_output,
675 size_t* output_size,
676 fz::stream_t stream = 0,
677 PipelinePerfResult* perf_out = nullptr,
678 size_t pool_override_bytes = 0
679 );
680
698 void decompressFromFileInstance(
699 const std::string& filename,
700 void** d_output,
701 size_t* output_size,
702 fz::stream_t stream = 0,
703 PipelinePerfResult* perf_out = nullptr
704 );
705
730 void decompressFromMemory(
731 const void* header_bytes,
732 size_t header_size,
733 const void* d_blob,
734 size_t blob_size,
735 void** d_output,
736 size_t* output_size,
737 fz::stream_t stream = 0
738 );
739
740 // ── Config File ───────────────────────────────────────────────────────────
741
755 void loadConfig(const std::string& path);
756
765 void saveConfig(const std::string& path) const;
766
767private:
768 // ── RAII buffer wrappers (private implementation detail) ─────────────────
769
770 // Pool-allocated persistent device buffer.
771 struct PoolBuffer {
772 void* ptr = nullptr;
773 size_t capacity = 0;
774 MemoryPool* pool = nullptr;
775
776 ~PoolBuffer() { free(0); }
777 PoolBuffer() = default;
778 PoolBuffer(const PoolBuffer&) = delete;
779 PoolBuffer& operator=(const PoolBuffer&) = delete;
780
781 void free(fz::stream_t s) {
782 if (ptr && pool) { pool->free(ptr, s); ptr = nullptr; capacity = 0; }
783 }
784 bool allocate(MemoryPool* p, size_t bytes, fz::stream_t s,
785 const char* tag, bool persistent = false) {
786 free(s);
787 pool = p;
788 ptr = pool->allocate(bytes, s, tag, persistent);
789 if (ptr) capacity = bytes;
790 return ptr != nullptr;
791 }
792 };
793
794 // cudaHostAlloc pinned host buffer — grows on demand, never shrinks.
795 struct PinnedBuffer {
796 void* ptr = nullptr;
797 size_t capacity = 0;
798
799 ~PinnedBuffer() { if (ptr) cudaFreeHost(ptr); }
800 PinnedBuffer() = default;
801 PinnedBuffer(const PinnedBuffer&) = delete;
802 PinnedBuffer& operator=(const PinnedBuffer&) = delete;
803
804 // Returns false on CUDA allocation failure.
805 bool ensureCapacity(size_t bytes) {
806 if (capacity >= bytes) return true;
807 if (ptr) { cudaFreeHost(ptr); ptr = nullptr; capacity = 0; }
808 if (cudaHostAlloc(&ptr, bytes, cudaHostAllocDefault) != cudaSuccess) return false;
809 capacity = bytes;
810 return true;
811 }
812 };
813
814 // cudaMalloc device buffer — grows on demand, never shrinks.
815 struct DeviceBuffer {
816 void* ptr = nullptr;
817 size_t capacity = 0;
818
819 ~DeviceBuffer() { if (ptr) cudaFree(ptr); }
820 DeviceBuffer() = default;
821 DeviceBuffer(const DeviceBuffer&) = delete;
822 DeviceBuffer& operator=(const DeviceBuffer&) = delete;
823
824 // Returns false on CUDA allocation failure.
825 bool ensureCapacity(size_t bytes) {
826 if (capacity >= bytes) return true;
827 if (ptr) { cudaFree(ptr); ptr = nullptr; capacity = 0; }
828 if (cudaMalloc(&ptr, bytes) != cudaSuccess) return false;
829 capacity = bytes;
830 return true;
831 }
832 };
833
834 // ── Internal helpers ──────────────────────────────────────────────────────
835
836 Stage* addRawStage(Stage* stage);
837
838 struct OutputBuffer {
839 void* d_ptr;
840 size_t actual_size;
841 size_t allocated_size;
842 std::string name;
843 int buffer_id;
844 };
845 std::vector<OutputBuffer> getOutputBuffers() const;
846
847 static void* loadCompressedData(
848 const std::string& filename,
849 const FZMFileHeader& header,
850 fz::stream_t stream = 0,
851 MemoryPool* pool = nullptr
852 );
853
854 void validate();
855 std::pair<std::vector<Stage*>, std::vector<Stage*>> identifyTopology();
856 void setupInputBuffers(const std::vector<Stage*>& sources);
857 int autoDetectUnconnectedOutputs();
858 void detectMultiOutputScenario(int pipeline_outputs);
859 void configureStreamsIfNeeded();
860
861 // finalize() sub-steps
862 void typeCheckConnections();
863 void computeInputAlignment();
866 void bindSemanticContracts();
870 void planAndInstallFusion();
871 void notifyStagesFinalizeHooks();
872 void refinePoolSize();
873 void setupGraphModeInput();
874 void preallocatePadBuffer();
875 void preallocateConcatBuffers();
876
877 // compress() helper: handles graph-mode copy or alignment padding.
878 // Returns the effective source pointer and padded source size.
879 std::pair<const void*, size_t> prepareInputSource(
880 const void* d_input, size_t input_size, fz::stream_t stream);
881
887 void propagateBufferSizes(bool force_from_current_inputs = false);
888
889 std::vector<Stage*> getSourceStages() const;
890 std::vector<Stage*> getSinkStages() const;
891
892 // ── Inverse DAG helpers ───────────────────────────────────────────────────
893
895 struct FwdStageDesc {
896 Stage* stage;
897 std::vector<int> output_buf_ids;
898 std::vector<int> input_buf_ids;
899 };
900
902 using PipelineOutputMap = std::unordered_map<int, std::pair<void*, size_t>>;
903
904 // decompress() helper: builds or reuses the inverse DAG cache.
905 void buildOrReuseInvCache(
906 const PipelineOutputMap& po_map,
907 Stage* src_stage,
908 size_t src_sz,
909 fz::stream_t stream);
910
923 void decompressCore(
924 const void* d_input,
925 size_t input_size,
926 void* caller_output,
927 size_t caller_capacity,
928 bool synchronize,
929 void** d_output,
930 size_t* output_size,
931 fz::stream_t stream);
932
940 void buildStaticBufferMetadata();
941
947 std::vector<size_t> readConcatSegmentSizes(
948 const void* d_blob, size_t n, fz::stream_t stream) const;
949
950 // decompressFromFile() helpers.
952 static FZMFileHeader parseHeaderFromMemory(const void* data, size_t size);
953 static size_t computeFilePoolSize(const FZMFileHeader& fh, size_t pool_override_bytes);
954 static std::pair<std::vector<std::unique_ptr<Stage>>, std::vector<FwdStageDesc>>
955 reconstructForwardTopology(const FZMFileHeader& fh);
956 static std::unordered_map<Stage*, size_t> buildSourceSizesFromHeader(
957 const FZMFileHeader& fh, const std::vector<FwdStageDesc>& fwd_topology);
958
964 static std::pair<std::unique_ptr<CompressionDAG>,
965 std::unordered_map<Stage*, int>>
966 buildInverseDAG(
967 const std::vector<FwdStageDesc>& fwd_stages,
968 const PipelineOutputMap& pipeline_outputs,
969 MemoryPool* pool,
970 MemoryStrategy strategy,
971 const std::unordered_map<Stage*, size_t>& source_sizes,
972 bool enable_profiling
973 );
974
975 // ── Concat helpers ────────────────────────────────────────────────────────
976
977 struct OutputBufferInfo {
978 int buffer_id;
979 void* d_ptr;
980 size_t actual_size;
981 std::string stage_name;
982 std::string output_name;
983 };
984
985 std::vector<OutputBufferInfo> collectOutputBuffers() const;
986
988 size_t calculateConcatSize(const std::vector<OutputBufferInfo>& outputs) const;
989
990 size_t writeConcatBuffer(
991 const std::vector<OutputBufferInfo>& outputs,
992 uint8_t* d_concat_bytes,
993 fz::stream_t stream
994 ) const;
995
996 void concatOutputs(void** d_output, size_t* output_size, fz::stream_t stream);
997
998 // ── Member variables ──────────────────────────────────────────────────────
999
1000 std::unique_ptr<MemoryPool> mem_pool_;
1001 std::unique_ptr<CompressionDAG> dag_;
1002 MemoryStrategy strategy_;
1003
1004 std::vector<std::unique_ptr<Stage>> stages_;
1005 std::unordered_map<Stage*, DAGNode*> stage_to_node_;
1006
1007 struct ConnectionInfo {
1008 Stage* dependent;
1009 Stage* producer;
1010 std::string output_name;
1011 int output_index;
1012 };
1013 std::vector<ConnectionInfo> connections_;
1014
1015 int num_streams_;
1016 bool is_finalized_;
1017 bool warmup_on_finalize_;
1018 bool pool_managed_decomp_;
1019
1020 // is_compressed_: true after the first successful compress() (gates writeToFile).
1021 // was_compressed_: true between compress() and the next reset() (gates captureGraph).
1022 bool is_compressed_;
1023 bool was_compressed_;
1024
1025 bool profiling_enabled_;
1028 bool coloring_enabled_ = true;
1029 PipelinePerfResult last_perf_result_;
1030
1031 std::vector<DAGNode*> input_nodes_;
1032 // Explicit choice of which input_nodes_ entry decompress() returns, for
1033 // pipelines with more than one source stage. Null = default to
1034 // input_nodes_[0]. Set via setPrimarySource(); resolved to an index lazily
1035 // (source stages aren't wired into input_nodes_ until finalize()).
1036 Stage* primary_source_stage_ = nullptr;
1037 // (node, buffer_id) pairs registered by bindExternalInput(), captured
1038 // immediately (before any later connect() calls add more buffer ids to
1039 // the same node). setupInputBuffers() appends these to input_nodes_/
1040 // input_buffer_ids_ after its own auto-discovery pass, since it clears
1041 // both first and auto-discovery alone would miss any stage that also
1042 // has real dependencies (getSourceStages() requires dependencies.empty()).
1043 std::vector<std::pair<DAGNode*, int>> explicit_external_bindings_;
1044 std::vector<DAGNode*> output_nodes_;
1045 std::vector<int> input_buffer_ids_;
1046 std::vector<int> output_buffer_ids_;
1047
1048 PoolBuffer d_concat_buffer_;
1049 bool needs_concat_;
1050
1051 // Pool-persistent decompress output buffers (one per source stage).
1052 // Only used when pool_managed_decomp_ == true.
1053 std::vector<void*> d_decomp_outputs_;
1054
1055 // Pinned host buffer for concat header (one H2D copy instead of N).
1056 PinnedBuffer h_concat_header_;
1057 // Persistent pinned host + device descriptor buffers for the gather kernel.
1058 PinnedBuffer h_copy_descs_;
1059 DeviceBuffer d_copy_descs_;
1060
1061 size_t input_size_;
1062
1063 // Per-source input sizes from the most recent compress(), ordered to match
1064 // input_nodes_. Used by decompress() to size each inverse result buffer.
1065 std::vector<size_t> source_input_sizes_;
1066
1067 // Input alignment in bytes — LCM of all stage getRequiredInputAlignment() values.
1068 // compress() zero-pads to this boundary transparently.
1069 size_t input_alignment_bytes_;
1070 PoolBuffer d_pad_buf_;
1071
1072 // Original (pre-padding) input size. decompress() uses this to trim the
1073 // reported output back to what the caller provided. 0 when no padding.
1074 size_t original_input_size_;
1075
1076 size_t input_size_hint_;
1077 float pool_multiplier_;
1078
1079 // Dataset dimensions (x=fast, y, z). Pushed to each stage on addStage() and
1080 // again at finalize(). Default {0,1,1} = 1-D, infer x from input size.
1081 std::array<size_t, 3> dims_;
1082
1091 struct InvDAGCache {
1092 std::unique_ptr<CompressionDAG> inv_dag;
1093 std::unordered_map<Stage*, int> inv_result_map;
1094 std::unordered_map<int, int> fwd_to_inv_ext_buf;
1095 std::unordered_map<Stage*, size_t> source_sizes;
1096 };
1097 std::unique_ptr<InvDAGCache> inv_cache_;
1098
1099 struct BufferMetadata {
1100 int buffer_id;
1101 size_t actual_size;
1102 size_t allocated_size;
1103 std::string name;
1104 DAGNode* producer;
1105 int output_index;
1106 };
1107 std::vector<BufferMetadata> buffer_metadata_;
1108
1109 bool graph_mode_enabled_;
1110 bool graph_captured_;
1111 FusionPolicy fusion_policy_ = FusionPolicy::Off;
1112 FusionInfo fusion_info_;
1113
1114 // Fixed device input buffer whose address is baked into the captured graph.
1115 // compress() copies user input here before cudaGraphLaunch().
1116 PoolBuffer d_graph_input_;
1117 size_t d_graph_input_size_;
1118
1119 fz::graph_t captured_graph_;
1120 fz::graph_exec_t graph_exec_;
1121};
1122
1123// ── Template implementation ───────────────────────────────────────────────────
1124
1125template<typename StageT, typename... Args>
1126StageT* Pipeline::addStage(Args&&... args) {
1127 if (is_finalized_) {
1128 throw std::runtime_error("Cannot add stages after finalization");
1129 }
1130
1131 // if constexpr, not a runtime check: on an unsupported backend, StageT's
1132 // constructor may not exist in the build at all (its .cu translation
1133 // unit excluded — see Stage::isSupportedOnBackend()'s doc comment), so
1134 // every line below that references `new StageT()` must never be
1135 // instantiated at all, not merely never executed — hence the whole rest
1136 // of the function lives in the `if constexpr` branch rather than after
1137 // a standalone early-throw.
1138 if constexpr (!StageT::isSupportedOnBackend()) {
1139 throw std::runtime_error(
1140 "addStage(): this stage type is not supported on the current "
1141 "GPU backend (FZGMOD_BACKEND) this library was built for");
1142 } else {
1143 auto stage_ptr = std::make_unique<StageT>(std::forward<Args>(args)...);
1144 StageT* stage = stage_ptr.get();
1145
1146 stage->setDims(dims_);
1147
1148 DAGNode* node = dag_->addStage(stage, stage->getName());
1149 size_t num_outputs = stage->getNumOutputs();
1150 auto output_names = stage->getOutputNames();
1151
1152 // Pre-allocate all output slots as unconnected (size=1 placeholder).
1153 // connect() will promote any that get wired to downstream stages.
1154 for (size_t i = 0; i < num_outputs; i++) {
1155 std::string out_name = i < output_names.size() ? output_names[i] : std::to_string(i);
1156 dag_->addUnconnectedOutput(node, 1, i, stage->getName() + "." + out_name + "_unconnected");
1157 }
1158
1159 stage_to_node_[stage] = node;
1160 stages_.push_back(std::move(stage_ptr));
1161 return stage;
1162 }
1163}
1164
1165} // namespace fz
Definition mempool.h:82
void free(void *ptr, fz::stream_t stream)
void * allocate(size_t size, fz::stream_t stream, const std::string &tag="", bool persistent=false)
TOML-based pipeline configuration file support.
Compression DAG wiring, execution, and memory strategy types.
Backend-neutral device span and buffer value types.
FZM binary file format definitions — structs, enums, and helpers.
Stream-ordered CUDA memory pool for pipeline buffer management.
Definition dag.h:24
MemoryStrategy
Definition dag.h:32
@ MINIMAL
Allocate on-demand, free at last consumer. Lowest peak memory.
FusionPolicy
Definition compressor.h:48
Pipeline and per-stage profiling result types.
Base class interface for all compression stages.
Backward-compatible shim.
Fixed-size FZM file header core (80 bytes).
Definition fzm_format.h:272
One finalize-time fusion specialization selected for compress execution.
Definition compressor.h:51
Resolved fusion decision for diagnostics and benchmark provenance.
Definition compressor.h:57
std::string fallback_reason
policy_off, no_legal_group, or no_profitable_implementation; empty on a hit.
Definition compressor.h:62
Definition perf.h:78
Definition compressor.h:604
Backend-neutral GPU type aliases.