1515 * GC9 - isGraphCompatible() false for RZE inverse, true for all forward stages
1616 * GC10 - graph mode with coloring disabled still produces correct output
1717 * GC11 - warmup() before captureGraph() does not break subsequent graph compress
18+ * GC12 - graph mode with mempool fallback throws at finalize()
19+ * GC13 - captureGraph() without enableGraphMode() throws
20+ * GC14 - captureGraph() after compress() throws
21+ * GC15 - enableGraphMode() after finalize() throws
22+ * GC16 - graph capture works with a Lorenzo -> RLE pipeline
1823 */
1924
2025#include < gtest/gtest.h>
2126#include " helpers/fz_test_utils.h"
2227#include " fzgpumodules.h"
2328
2429#include < cmath>
30+ #include < cstdlib>
31+ #include < string>
2532#include < vector>
2633
2734using namespace fz ;
@@ -39,6 +46,29 @@ static std::vector<float> make_smooth(size_t n) {
3946 return v;
4047}
4148
49+ struct ScopedEnvVar {
50+ std::string name;
51+ bool had_old = false ;
52+ std::string old_value;
53+
54+ ScopedEnvVar (const char * var_name, const char * value) : name(var_name) {
55+ const char * old = std::getenv (var_name);
56+ if (old) {
57+ had_old = true ;
58+ old_value = old;
59+ }
60+ setenv (var_name, value, /* overwrite=*/ 1 );
61+ }
62+
63+ ~ScopedEnvVar () {
64+ if (had_old) {
65+ setenv (name.c_str (), old_value.c_str (), /* overwrite=*/ 1 );
66+ } else {
67+ unsetenv (name.c_str ());
68+ }
69+ }
70+ };
71+
4272// Build a finalized graph-mode pipeline with a single Lorenzo stage.
4373// Caller owns the pipeline; the graph stream must be non-default.
4474static std::unique_ptr<Pipeline> make_graph_pipeline (size_t in_bytes, bool disable_coloring = false ) {
@@ -54,10 +84,34 @@ static std::unique_ptr<Pipeline> make_graph_pipeline(size_t in_bytes, bool disab
5484 return p;
5585}
5686
87+ static bool is_graph_supported () {
88+ Pipeline p (1024 , MemoryStrategy::PREALLOCATE , 1 .0f );
89+ return !p.isMemPoolFallbackMode ();
90+ }
91+
92+ static std::unique_ptr<Pipeline> make_graph_pipeline_with_rle (size_t in_bytes) {
93+ auto p = std::make_unique<Pipeline>(in_bytes, MemoryStrategy::PREALLOCATE , 4 .0f );
94+ auto * lrz = p->addStage <LorenzoQuantStage<float , uint16_t >>();
95+ lrz->setErrorBound (1e-2f );
96+ lrz->setQuantRadius (512 );
97+ lrz->setOutlierCapacity (0 .2f );
98+
99+ auto * rle = p->addStage <RLEStage<uint16_t >>();
100+ p->connect (rle, lrz, " codes" );
101+
102+ p->enableGraphMode (true );
103+ p->setPoolManagedDecompOutput (false );
104+ p->finalize ();
105+ return p;
106+ }
107+
57108// ─────────────────────────────────────────────────────────────────────────────
58109// GC1: captureGraph() produces correct compressed + decompressed output
59110// ─────────────────────────────────────────────────────────────────────────────
60111TEST (GraphCapture, CorrectOutput) {
112+ if (!is_graph_supported ()) {
113+ GTEST_SKIP () << " Graph mode unsupported in cudaMalloc fallback mode" ;
114+ }
61115 constexpr size_t N = 1 << 14 ;
62116 constexpr float EB = 1e-2f ;
63117 const size_t in_bytes = N * sizeof (float );
@@ -105,6 +159,9 @@ TEST(GraphCapture, CorrectOutput) {
105159// semantically meaningful invariant is data correctness, not byte identity.
106160// ─────────────────────────────────────────────────────────────────────────────
107161TEST (GraphCapture, ReplayCorrect) {
162+ if (!is_graph_supported ()) {
163+ GTEST_SKIP () << " Graph mode unsupported in cudaMalloc fallback mode" ;
164+ }
108165 constexpr size_t N = 1 << 14 ;
109166 constexpr float EB = 1e-2f ;
110167 constexpr int ITERS = 12 ;
@@ -151,6 +208,9 @@ TEST(GraphCapture, ReplayCorrect) {
151208// filler may differ. The meaningful invariant is data correctness.
152209// ─────────────────────────────────────────────────────────────────────────────
153210TEST (GraphCapture, MatchesPreallocate) {
211+ if (!is_graph_supported ()) {
212+ GTEST_SKIP () << " Graph mode unsupported in cudaMalloc fallback mode" ;
213+ }
154214 constexpr size_t N = 1 << 14 ;
155215 constexpr float EB = 1e-2f ;
156216 const size_t in_bytes = N * sizeof (float );
@@ -268,6 +328,9 @@ TEST(GraphCapture, CaptureBeforeFinalizeThrows) {
268328// kernels. The pipeline must handle this gracefully and produce valid output.
269329// ─────────────────────────────────────────────────────────────────────────────
270330TEST (GraphCapture, CompressBeforeCaptureSucceeds) {
331+ if (!is_graph_supported ()) {
332+ GTEST_SKIP () << " Graph mode unsupported in cudaMalloc fallback mode" ;
333+ }
271334 constexpr size_t N = 1 << 13 ;
272335 constexpr float EB = 1e-2f ;
273336 const size_t in_bytes = N * sizeof (float );
@@ -380,6 +443,9 @@ TEST(GraphCapture, IsGraphCompatiblePerStage) {
380443// GC10: Graph mode with buffer coloring disabled still produces correct output
381444// ─────────────────────────────────────────────────────────────────────────────
382445TEST (GraphCapture, ColoringDisabledCorrectOutput) {
446+ if (!is_graph_supported ()) {
447+ GTEST_SKIP () << " Graph mode unsupported in cudaMalloc fallback mode" ;
448+ }
383449 constexpr size_t N = 1 << 13 ;
384450 constexpr float EB = 1e-2f ;
385451 const size_t in_bytes = N * sizeof (float );
@@ -426,6 +492,9 @@ TEST(GraphCapture, ColoringDisabledCorrectOutput) {
426492// still succeed and compress must produce correct output.
427493// ─────────────────────────────────────────────────────────────────────────────
428494TEST (GraphCapture, WarmupBeforeCaptureWorks) {
495+ if (!is_graph_supported ()) {
496+ GTEST_SKIP () << " Graph mode unsupported in cudaMalloc fallback mode" ;
497+ }
429498 constexpr size_t N = 1 << 14 ;
430499 constexpr float EB = 1e-2f ;
431500 const size_t in_bytes = N * sizeof (float );
@@ -466,3 +535,127 @@ TEST(GraphCapture, WarmupBeforeCaptureWorks) {
466535 EXPECT_LE (max_abs_error (h_input, h_recon), EB * 1 .01f )
467536 << " GC11: warmup→capture round-trip error exceeds bound" ;
468537}
538+
539+ // ─────────────────────────────────────────────────────────────────────────────
540+ // GC12: Graph mode must throw when mempool fallback is active (vGPU / forced)
541+ // ─────────────────────────────────────────────────────────────────────────────
542+ TEST (GraphCapture, MempoolFallbackThrowsAtFinalize) {
543+ ScopedEnvVar force_fallback (" FZ_FORCE_MEMPOOL_FALLBACK" , " 1" );
544+ constexpr size_t N = 1024 ;
545+ const size_t in_bytes = N * sizeof (float );
546+
547+ Pipeline p (in_bytes, MemoryStrategy::PREALLOCATE , 4 .0f );
548+ auto * lrz = p.addStage <LorenzoQuantStage<float , uint16_t >>();
549+ lrz->setErrorBound (1e-2f );
550+ p.enableGraphMode (true );
551+
552+ EXPECT_THROW (p.finalize (), std::runtime_error)
553+ << " GC12: graph mode must be rejected when mempool fallback is active" ;
554+ }
555+
556+ // ─────────────────────────────────────────────────────────────────────────────
557+ // GC13: captureGraph() without enableGraphMode() throws
558+ // ─────────────────────────────────────────────────────────────────────────────
559+ TEST (GraphCapture, CaptureWithoutEnableThrows) {
560+ constexpr size_t N = 1024 ;
561+ const size_t in_bytes = N * sizeof (float );
562+
563+ Pipeline p (in_bytes, MemoryStrategy::PREALLOCATE , 4 .0f );
564+ auto * lrz = p.addStage <LorenzoQuantStage<float , uint16_t >>();
565+ lrz->setErrorBound (1e-2f );
566+ p.finalize ();
567+
568+ CudaStream stream;
569+ EXPECT_THROW (p.captureGraph (stream), std::runtime_error)
570+ << " GC13: captureGraph() without enableGraphMode() must throw" ;
571+ }
572+
573+ // ─────────────────────────────────────────────────────────────────────────────
574+ // GC14: captureGraph() after compress() throws (must capture before first run)
575+ // ─────────────────────────────────────────────────────────────────────────────
576+ TEST (GraphCapture, CaptureAfterCompressThrows) {
577+ if (!is_graph_supported ()) {
578+ GTEST_SKIP () << " Graph mode unsupported in cudaMalloc fallback mode" ;
579+ }
580+ constexpr size_t N = 1 << 12 ;
581+ const size_t in_bytes = N * sizeof (float );
582+
583+ auto h_input = make_smooth (N);
584+ CudaStream stream;
585+ CudaBuffer<float > d_in (N);
586+ d_in.upload (h_input, stream);
587+ stream.sync ();
588+
589+ auto p = make_graph_pipeline (in_bytes);
590+
591+ void * d_comp = nullptr ;
592+ size_t comp_sz = 0 ;
593+ p->compress (d_in.void_ptr (), in_bytes, &d_comp, &comp_sz, stream);
594+ stream.sync ();
595+ ASSERT_GT (comp_sz, 0u );
596+
597+ EXPECT_THROW (p->captureGraph (stream), std::runtime_error)
598+ << " GC14: captureGraph() after compress() must throw" ;
599+ }
600+
601+ // ─────────────────────────────────────────────────────────────────────────────
602+ // GC15: enableGraphMode() after finalize() throws
603+ // ─────────────────────────────────────────────────────────────────────────────
604+ TEST (GraphCapture, EnableAfterFinalizeThrows) {
605+ constexpr size_t N = 1024 ;
606+ Pipeline p (N * sizeof (float ), MemoryStrategy::PREALLOCATE , 4 .0f );
607+ auto * lrz = p.addStage <LorenzoQuantStage<float , uint16_t >>();
608+ lrz->setErrorBound (1e-2f );
609+ p.finalize ();
610+
611+ EXPECT_THROW (p.enableGraphMode (true ), std::runtime_error)
612+ << " GC15: enableGraphMode() after finalize() must throw" ;
613+ }
614+
615+ // ─────────────────────────────────────────────────────────────────────────────
616+ // GC16: Graph capture works for a Lorenzo → RLE pipeline
617+ // ─────────────────────────────────────────────────────────────────────────────
618+ TEST (GraphCapture, RlePipelineCorrectOutput) {
619+ if (!is_graph_supported ()) {
620+ GTEST_SKIP () << " Graph mode unsupported in cudaMalloc fallback mode" ;
621+ }
622+ constexpr size_t N = 1 << 14 ;
623+ constexpr float EB = 1e-2f ;
624+ const size_t in_bytes = N * sizeof (float );
625+
626+ auto h_input = make_smooth (N);
627+ CudaStream stream;
628+ CudaBuffer<float > d_in (N);
629+ d_in.upload (h_input, stream);
630+ stream.sync ();
631+
632+ auto p = make_graph_pipeline_with_rle (in_bytes);
633+
634+ // Warmup to pre-allocate stage scratch and host buffers outside the capture window.
635+ p->warmup (stream);
636+ stream.sync ();
637+
638+ p->captureGraph (stream);
639+ ASSERT_TRUE (p->isGraphCaptured ());
640+
641+ void * d_comp = nullptr ;
642+ size_t comp_sz = 0 ;
643+ p->compress (d_in.void_ptr (), in_bytes, &d_comp, &comp_sz, stream);
644+ stream.sync ();
645+ ASSERT_GT (comp_sz, 0u );
646+
647+ void * d_dec = nullptr ;
648+ size_t dec_sz = 0 ;
649+ p->decompress (nullptr , 0 , &d_dec, &dec_sz, stream);
650+ stream.sync ();
651+ ASSERT_NE (d_dec, nullptr );
652+ ASSERT_EQ (dec_sz, in_bytes);
653+
654+ std::vector<float > h_recon (N);
655+ cudaMemcpy (h_recon.data (), d_dec, in_bytes, cudaMemcpyDeviceToHost);
656+ cudaFree (d_dec);
657+
658+ float max_err = max_abs_error (h_input, h_recon);
659+ EXPECT_LE (max_err, EB * 1 .01f )
660+ << " GC16: Lorenzo->RLE graph round-trip max_err=" << max_err;
661+ }
0 commit comments