Repository: InsightSoftwareConsortium/ITKVkFFTBackend (language: python)

Issue:
Platform cannot be selected in factory-instantiated classes
When directly using a class such as `itk.VkForwardFFTImageFilter` it is easy to set the OpenCL platform to run on (such as CPU vs GPU) with `fft_filter.SetDeviceID(<val>)`. However, when the class is consumed via factory override, the answer is less clear. This gets at a more fundamental question: _how do we set properties unique to the derived class when instantiated through the object factory_?

In this case the OpenCL platform to use is unique to the user, but will not change very much across VkFFT filters.

Possible solutions:
- Have VkFFT classes consume a global variable defining the platform ID to use: not pretty, but quick and functional
- Update the ITK object factory mechanism to include user-settable constructor arguments with class overrides: helps avoid this problem in the long run, but would take a while to architect and implement, and might get messy
- Define class overrides for individual ITK classes known to consume FFT filters, then expose the platform arg in derived classes: operates within existing architecture constraints, but also partially defeats the point of object factory overrides in that we now have to cover every class using FFTs.

In the interest of maximizing effort spent I think the first bullet is the most effective path for now.

ENH: Add default factory initialization
Leverages the `InitFactory` mechanism introduced in ITK commit c266651
to register VkFFT GPU-accelerated implementation classes as the default
backends for ITK FFT classes via the object factory.

With these changes in place, GPU acceleration of ITK FFT image filters is achieved by simply installing the `itk-vkfftbackend` Python module:
```py
> pip install itk itk-vkfftbackend
> python
>>> import itk
>>> fft_filter = itk.ForwardFFTImageFilter.values()[-1].New()        # base FFT filter instantiated
>>> t1 = itk.VkForwardFFTImageFilter.values()[-1]                  # Prove instance is VkFFT backend with a cast
>>> t1.cast(fft_filter)
<itk.itkVkForwardFFTImageFilterPython.itkVkForwardFFTImageFilterIF3; proxy of <Swig Object of type 'itkVkForwardFFTImageFilterIF3 *' at 0x0000016275980270> >
```

This PR relies on ITK features that will be available in the ITK 5.3rc04 prerelease, may need to wait for that release in order for CI to pass. I have verified tests pass on my local Windows system when built against the ITK main branch.


Discussion / hints:
Digging further, it seems that device/platform IDs are set by instantiating a `VkGPU` struct internal to each filter, setting properties, then passing the struct to VkFFT procedures.

Proposal:
- Add static setters in `VkGPU` allowing the user to override default properties
- Modify the `VkGPU` default constructor to use default properties
- Modify FFT filters so that platform ID is only altered if it has been explicitly set by the user, otherwise use defaults
- Wrap `VkGPU` struct so that global setters are accessible in Python


Fix patch:
diff --git a/include/itkVkComplexToComplex1DFFTImageFilter.h b/include/itkVkComplexToComplex1DFFTImageFilter.h
index 2a1848c0..883a0213 100644
--- a/include/itkVkComplexToComplex1DFFTImageFilter.h
+++ b/include/itkVkComplexToComplex1DFFTImageFilter.h
@@ -19,9 +19,10 @@
 #ifndef itkVkComplexToComplex1DFFTImageFilter_h
 #define itkVkComplexToComplex1DFFTImageFilter_h
 
+#include "itkComplexToComplex1DFFTImageFilter.h"
 #include "itkFFTImageFilterFactory.h"
 #include "itkVkCommon.h"
-#include "itkComplexToComplex1DFFTImageFilter.h"
+#include "itkVkGlobalConfiguration.h"
 
 namespace itk
 {
@@ -34,9 +35,9 @@ namespace itk
  *  the VkFFT library.
  *
  * This filter is multithreaded and by default supports input images with sizes which are
- * divisible by primes up to 13. 
- * 
- * Execution on input images with sizes divisible by primes greater than 17 may succeed 
+ * divisible only by primes up to 13.
+ *
+ * Execution on input images with sizes divisible by primes greater than 13 may succeed
  * with a fallback on Bluestein's algorithm per VkFFT with a cost to performance and output precision.
  *
  * \ingroup FourierTransform
@@ -81,9 +82,25 @@ class VkComplexToComplex1DFFTImageFilter : public ComplexToComplex1DFFTImageFilt
 
   static constexpr unsigned int ImageDimension = InputImageType::ImageDimension;
 
-  itkGetMacro(DeviceID, uint64_t);
+  /** Determine whether local or global properties will be
+   *  referenced for setting up GPU acceleration.
+   *  Defaults to global so that the user can adjust default properties
+   *  in filters constructed through the ITK object factory. */
+  itkSetMacro(UseVkGlobalConfiguration, bool);
+  itkGetMacro(UseVkGlobalConfiguration, bool);
+
+  /** Local setting for enumerated GPU device to use for FFT.
+   *  Ignored if `UseVkGlobalConfiguration` is true. */
   itkSetMacro(DeviceID, uint64_t);
 
+  /** Return the enumerated GPU device to use for FFT
+   *  according to current filter settings. */
+  uint64_t
+  GetDeviceID() const
+  {
+    return m_UseVkGlobalConfiguration ? VkGlobalConfiguration::GetDeviceID() : m_DeviceID;
+  }
+
   SizeValueType
   GetSizeGreatestPrimeFactor() const override;
 
@@ -98,8 +115,8 @@ class VkComplexToComplex1DFFTImageFilter : public ComplexToComplex1DFFTImageFilt
   PrintSelf(std::ostream & os, Indent indent) const override;
 
 private:
+  bool     m_UseVkGlobalConfiguration{true};
   uint64_t m_DeviceID{ 0UL };
-
   VkCommon m_VkCommon{};
 };
 
diff --git a/include/itkVkComplexToComplex1DFFTImageFilter.hxx b/include/itkVkComplexToComplex1DFFTImageFilter.hxx
index 7692ea1d..1e5fa7e4 100644
--- a/include/itkVkComplexToComplex1DFFTImageFilter.hxx
+++ b/include/itkVkComplexToComplex1DFFTImageFilter.hxx
@@ -20,6 +20,7 @@
 #define itkVkComplexToComplex1DFFTImageFilter_hxx
 
 #include "itkVkComplexToComplex1DFFTImageFilter.h"
+#include "itkVkGlobalConfiguration.h"
 #include "vkFFT.h"
 #include "itkImageRegionIterator.h"
 #include "itkIndent.h"
@@ -61,7 +62,7 @@ VkComplexToComplex1DFFTImageFilter<TInputImage, TOutputImage>::GenerateData()
 
   // Mostly use defaults for VkCommon::VkGPU
   typename VkCommon::VkGPU vkGPU;
-  vkGPU.device_id = m_DeviceID;
+  vkGPU.device_id = this->GetDeviceID();
 
   // Describe this filter in VkCommon::VkParameters
   typename VkCommon::VkParameters vkParameters;
@@ -112,7 +113,10 @@ void
 VkComplexToComplex1DFFTImageFilter<TInputImage, TOutputImage>::PrintSelf(std::ostream & os, Indent indent) const
 {
   Superclass::PrintSelf(os, indent);
-  os << indent << "DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "UseVkGlobalConfiguration: " << m_UseVkGlobalConfiguration << std::endl;
+  os << indent << "Local DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "Global DeviceID: " << VkGlobalConfiguration::GetDeviceID() << std::endl;
+  os << indent << "Preferred DeviceID: " << this->GetDeviceID() << std::endl;
 }
 
 template <typename TInputImage, typename TOutputImage>
diff --git a/include/itkVkComplexToComplexFFTImageFilter.h b/include/itkVkComplexToComplexFFTImageFilter.h
index eb5e60c8..174caec1 100644
--- a/include/itkVkComplexToComplexFFTImageFilter.h
+++ b/include/itkVkComplexToComplexFFTImageFilter.h
@@ -19,9 +19,10 @@
 #ifndef itkVkComplexToComplexFFTImageFilter_h
 #define itkVkComplexToComplexFFTImageFilter_h
 
+#include "itkComplexToComplexFFTImageFilter.h"
 #include "itkFFTImageFilterFactory.h"
 #include "itkVkCommon.h"
-#include "itkComplexToComplexFFTImageFilter.h"
+#include "itkVkGlobalConfiguration.h"
 
 namespace itk
 {
@@ -34,7 +35,7 @@ namespace itk
  *  the VkFFT library.
  *
  * This filter is multithreaded and supports input images with sizes which are
- * divisible by primes up to 13.
+ * divisible only by primes up to 13.
  *
  * \ingroup FourierTransform
  * \ingroup MultiThreaded
@@ -78,9 +79,25 @@ class VkComplexToComplexFFTImageFilter : public ComplexToComplexFFTImageFilter<T
 
   static constexpr unsigned int ImageDimension = InputImageType::ImageDimension;
 
-  itkGetMacro(DeviceID, uint64_t);
+  /** Determine whether local or global properties will be
+   *  referenced for setting up GPU acceleration.
+   *  Defaults to global so that the user can adjust default properties
+   *  in filters constructed through the ITK object factory. */
+  itkSetMacro(UseVkGlobalConfiguration, bool);
+  itkGetMacro(UseVkGlobalConfiguration, bool);
+
+  /** Local setting for enumerated GPU device to use for FFT.
+   *  Ignored if `UseVkGlobalConfiguration` is true. */
   itkSetMacro(DeviceID, uint64_t);
 
+  /** Return the enumerated GPU device to use for FFT
+   *  according to current filter settings. */
+  uint64_t
+  GetDeviceID() const
+  {
+    return m_UseVkGlobalConfiguration ? VkGlobalConfiguration::GetDeviceID() : m_DeviceID;
+  }
+
   SizeValueType
   GetSizeGreatestPrimeFactor() const;
 
@@ -95,6 +112,7 @@ class VkComplexToComplexFFTImageFilter : public ComplexToComplexFFTImageFilter<T
   PrintSelf(std::ostream & os, Indent indent) const override;
 
 private:
+  bool     m_UseVkGlobalConfiguration{true};
   uint64_t m_DeviceID{ 0UL };
 
   VkCommon m_VkCommon{};
diff --git a/include/itkVkComplexToComplexFFTImageFilter.hxx b/include/itkVkComplexToComplexFFTImageFilter.hxx
index 04b42a91..8877b23a 100644
--- a/include/itkVkComplexToComplexFFTImageFilter.hxx
+++ b/include/itkVkComplexToComplexFFTImageFilter.hxx
@@ -66,7 +66,7 @@ VkComplexToComplexFFTImageFilter<TInputImage, TOutputImage>::GenerateData()
 
   // Mostly use defaults for VkCommon::VkGPU
   typename VkCommon::VkGPU vkGPU;
-  vkGPU.device_id = m_DeviceID;
+  vkGPU.device_id = this->GetDeviceID();
 
   // Describe this filter in VkCommon::VkParameters
   typename VkCommon::VkParameters vkParameters;
@@ -110,7 +110,10 @@ void
 VkComplexToComplexFFTImageFilter<TInputImage, TOutputImage>::PrintSelf(std::ostream & os, Indent indent) const
 {
   Superclass::PrintSelf(os, indent);
-  os << indent << "DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "UseVkGlobalConfiguration: " << m_UseVkGlobalConfiguration << std::endl;
+  os << indent << "Local DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "Global DeviceID: " << VkGlobalConfiguration::GetDeviceID() << std::endl;
+  os << indent << "Preferred DeviceID: " << this->GetDeviceID() << std::endl;
 }
 
 template <typename TInputImage, typename TOutputImage>
diff --git a/include/itkVkForward1DFFTImageFilter.h b/include/itkVkForward1DFFTImageFilter.h
index 7c975cba..cf71b7d2 100644
--- a/include/itkVkForward1DFFTImageFilter.h
+++ b/include/itkVkForward1DFFTImageFilter.h
@@ -21,6 +21,7 @@
 #include "itkFFTImageFilterFactory.h"
 #include "itkForward1DFFTImageFilter.h"
 #include "itkVkCommon.h"
+#include "itkVkGlobalConfiguration.h"
 
 namespace itk
 {
@@ -33,9 +34,9 @@ namespace itk
  * implementation is based on the VkFFT library.
  *
  * This filter is multithreaded and by default supports input images with sizes which are
- * divisible by primes up to 13.
- * 
- * Execution on input images with sizes divisible by primes greater than 17 may succeed 
+ * divisible only by primes up to 13.
+ *
+ * Execution on input images with sizes divisible by primes greater than 13 may succeed
  * with a fallback on Bluestein's algorithm per VkFFT with a cost to performance and output precision.
  *
  * \ingroup FourierTransform
@@ -48,8 +49,7 @@ namespace itk
  */
 template <typename TInputImage,
           typename TOutputImage = Image<std::complex<typename TInputImage::PixelType>, TInputImage::ImageDimension>>
-class VkForward1DFFTImageFilter
-  : public Forward1DFFTImageFilter<TInputImage, TOutputImage>
+class VkForward1DFFTImageFilter : public Forward1DFFTImageFilter<TInputImage, TOutputImage>
 {
 public:
   ITK_DISALLOW_COPY_AND_MOVE(VkForward1DFFTImageFilter);
@@ -86,9 +86,25 @@ class VkForward1DFFTImageFilter
 
   static constexpr unsigned int ImageDimension = InputImageType::ImageDimension;
 
-  itkGetMacro(DeviceID, uint64_t);
+  /** Determine whether local or global properties will be
+   *  referenced for setting up GPU acceleration.
+   *  Defaults to global so that the user can adjust default properties
+   *  in filters constructed through the ITK object factory. */
+  itkSetMacro(UseVkGlobalConfiguration, bool);
+  itkGetMacro(UseVkGlobalConfiguration, bool);
+
+  /** Local setting for enumerated GPU device to use for FFT.
+   *  Ignored if `UseVkGlobalConfiguration` is true. */
   itkSetMacro(DeviceID, uint64_t);
 
+  /** Return the enumerated GPU device to use for FFT
+   *  according to current filter settings. */
+  uint64_t
+  GetDeviceID() const
+  {
+    return m_UseVkGlobalConfiguration ? VkGlobalConfiguration::GetDeviceID() : m_DeviceID;
+  }
+
   SizeValueType
   GetSizeGreatestPrimeFactor() const override;
 
@@ -103,6 +119,7 @@ class VkForward1DFFTImageFilter
   PrintSelf(std::ostream & os, Indent indent) const override;
 
 private:
+  bool     m_UseVkGlobalConfiguration{true};
   uint64_t m_DeviceID{ 0UL };
 
   VkCommon m_VkCommon{};
diff --git a/include/itkVkForward1DFFTImageFilter.hxx b/include/itkVkForward1DFFTImageFilter.hxx
index 4580584f..a6f00a17 100644
--- a/include/itkVkForward1DFFTImageFilter.hxx
+++ b/include/itkVkForward1DFFTImageFilter.hxx
@@ -61,7 +61,7 @@ VkForward1DFFTImageFilter<TInputImage, TOutputImage>::GenerateData()
 
   // Mostly use defaults for VkCommon::VkGPU
   typename VkCommon::VkGPU vkGPU;
-  vkGPU.device_id = m_DeviceID;
+  vkGPU.device_id = this->GetDeviceID();
 
   // Describe this filter in VkCommon::VkParameters
   typename VkCommon::VkParameters vkParameters;
@@ -108,7 +108,10 @@ void
 VkForward1DFFTImageFilter<TInputImage, TOutputImage>::PrintSelf(std::ostream & os, Indent indent) const
 {
   Superclass::PrintSelf(os, indent);
-  os << indent << "DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "UseVkGlobalConfiguration: " << m_UseVkGlobalConfiguration << std::endl;
+  os << indent << "Local DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "Global DeviceID: " << VkGlobalConfiguration::GetDeviceID() << std::endl;
+  os << indent << "Preferred DeviceID: " << this->GetDeviceID() << std::endl;
 }
 
 template <typename TInputImage, typename TOutputImage>
diff --git a/include/itkVkForwardFFTImageFilter.h b/include/itkVkForwardFFTImageFilter.h
index b4ae9826..8708b5c2 100644
--- a/include/itkVkForwardFFTImageFilter.h
+++ b/include/itkVkForwardFFTImageFilter.h
@@ -22,6 +22,7 @@
 #include "itkForwardFFTImageFilter.h"
 #include "itkImage.h"
 #include "itkVkCommon.h"
+#include "itkVkGlobalConfiguration.h"
 
 namespace itk
 {
@@ -34,7 +35,7 @@ namespace itk
  * implementation is based on the VkFFT library.
  *
  * This filter is multithreaded and supports input images with sizes which are
- * divisible by primes up to 13.
+ * divisible only by primes up to 13.
  *
  * \ingroup FourierTransform
  * \ingroup MultiThreaded
@@ -83,9 +84,25 @@ class VkForwardFFTImageFilter : public ForwardFFTImageFilter<TInputImage, TOutpu
 
   static constexpr unsigned int ImageDimension = InputImageType::ImageDimension;
 
-  itkGetMacro(DeviceID, uint64_t);
+  /** Determine whether local or global properties will be
+   *  referenced for setting up GPU acceleration.
+   *  Defaults to global so that the user can adjust default properties
+   *  in filters constructed through the ITK object factory. */
+  itkSetMacro(UseVkGlobalConfiguration, bool);
+  itkGetMacro(UseVkGlobalConfiguration, bool);
+
+  /** Local setting for enumerated GPU device to use for FFT.
+   *  Ignored if `UseVkGlobalConfiguration` is true. */
   itkSetMacro(DeviceID, uint64_t);
 
+  /** Return the enumerated GPU device to use for FFT
+   *  according to current filter settings. */
+  uint64_t
+  GetDeviceID() const
+  {
+    return m_UseVkGlobalConfiguration ? VkGlobalConfiguration::GetDeviceID() : m_DeviceID;
+  }
+
   SizeValueType
   GetSizeGreatestPrimeFactor() const override;
 
@@ -100,6 +117,7 @@ class VkForwardFFTImageFilter : public ForwardFFTImageFilter<TInputImage, TOutpu
   PrintSelf(std::ostream & os, Indent indent) const override;
 
 private:
+  bool     m_UseVkGlobalConfiguration{true};
   uint64_t m_DeviceID{ 0UL };
 
   VkCommon m_VkCommon{};
diff --git a/include/itkVkForwardFFTImageFilter.hxx b/include/itkVkForwardFFTImageFilter.hxx
index 78b816cb..a77cdd15 100644
--- a/include/itkVkForwardFFTImageFilter.hxx
+++ b/include/itkVkForwardFFTImageFilter.hxx
@@ -66,7 +66,7 @@ VkForwardFFTImageFilter<TInputImage, TOutputImage>::GenerateData()
 
   // Mostly use defaults for VkCommon::VkGPU
   typename VkCommon::VkGPU vkGPU;
-  vkGPU.device_id = m_DeviceID;
+  vkGPU.device_id = this->GetDeviceID();
 
   // Describe this filter in VkCommon::VkParameters
   typename VkCommon::VkParameters vkParameters;
@@ -106,7 +106,10 @@ void
 VkForwardFFTImageFilter<TInputImage, TOutputImage>::PrintSelf(std::ostream & os, Indent indent) const
 {
   Superclass::PrintSelf(os, indent);
-  os << indent << "DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "UseVkGlobalConfiguration: " << m_UseVkGlobalConfiguration << std::endl;
+  os << indent << "Local DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "Global DeviceID: " << VkGlobalConfiguration::GetDeviceID() << std::endl;
+  os << indent << "Preferred DeviceID: " << this->GetDeviceID() << std::endl;
 }
 
 template <typename TInputImage, typename TOutputImage>
diff --git a/include/itkVkGlobalConfiguration.h b/include/itkVkGlobalConfiguration.h
new file mode 100644
index 00000000..88bedc04
--- /dev/null
+++ b/include/itkVkGlobalConfiguration.h
@@ -0,0 +1,86 @@
+/*=========================================================================
+ *
+ *  Copyright NumFOCUS
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0.txt
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *=========================================================================*/
+#ifndef itkVkGlobalConfiguration_h
+#define itkVkGlobalConfiguration_h
+
+#include "VkFFTBackendExport.h"
+#include "itkLightObject.h"
+#include "itkMacro.h"
+
+namespace itk
+{
+
+/**
+ * \class VkGlobalConfigurationGlobals
+ *  \brief Implementation detail to reference the VkGlobalConfiguration singleton
+ *  \ingroup VkFFTBackend
+ */
+struct VkGlobalConfigurationGlobals;
+
+/**
+ *\class VkGlobalConfiguration
+ *
+ *  \brief Implements a singleton instance for setting global VkFFT default parameters.
+ *
+ * \ingroup VkFFTBackend
+ */
+class VkFFTBackend_EXPORT VkGlobalConfiguration : public LightObject
+{
+public:
+  ITK_DISALLOW_COPY_AND_MOVE(VkGlobalConfiguration);
+
+  /** Standard class type aliases. */
+  using Self = VkGlobalConfiguration;
+  using Superclass = LightObject;
+  using Pointer = SmartPointer<Self>;
+  using ConstPointer = SmartPointer<const Self>;
+
+  /** Run-time type information (and related methods). */
+  itkTypeMacro(VkGlobalConfiguration, LightObject);
+
+  /** Default accelerated platform identifier */
+  static void
+  SetDeviceID(const uint64_t id);
+
+  /** Default accelerated platform identifier */
+  static uint64_t
+  GetDeviceID();
+
+private:
+  VkGlobalConfiguration() = default;
+  ~VkGlobalConfiguration() override = default;
+
+  /** Access synchronized global singleton */
+  static Pointer
+  GetInstance();
+
+  itkGetGlobalDeclarationMacro(VkGlobalConfigurationGlobals, PimplGlobals);
+
+  /** This is a singleton pattern New.  There will only be ONE
+   * reference to a VkGlobalConfiguration object per process.
+   * The single instance will be unreferenced when
+   * the program exits. */
+  itkFactorylessNewMacro(Self);
+
+  static VkGlobalConfigurationGlobals * m_PimplGlobals;
+
+  uint64_t m_DeviceID{ 0 };
+};
+} // namespace itk
+
+#endif // itkVkGlobalConfiguration_h
diff --git a/include/itkVkHalfHermitianToRealInverseFFTImageFilter.h b/include/itkVkHalfHermitianToRealInverseFFTImageFilter.h
index 7147d1c0..48567b35 100644
--- a/include/itkVkHalfHermitianToRealInverseFFTImageFilter.h
+++ b/include/itkVkHalfHermitianToRealInverseFFTImageFilter.h
@@ -22,6 +22,7 @@
 #include "itkHalfHermitianToRealInverseFFTImageFilter.h"
 #include "itkImage.h"
 #include "itkVkCommon.h"
+#include "itkVkGlobalConfiguration.h"
 
 namespace itk
 {
@@ -34,7 +35,7 @@ namespace itk
  * implementation is based on the VkFFT library.
  *
  * This filter is multithreaded and supports input images with sizes which are
- * divisible by primes up to 13.
+ * divisible only by primes up to 13.
  *
  * \ingroup FourierTransform
  * \ingroup MultiThreaded
@@ -47,8 +48,7 @@ namespace itk
 template <typename TInputImage,
           typename TOutputImage = Image<typename TInputImage::PixelType::value_type, TInputImage::ImageDimension>>
 class VkHalfHermitianToRealInverseFFTImageFilter
-  : public HalfHermitianToRealInverseFFTImageFilter<
-      TInputImage, TOutputImage>
+  : public HalfHermitianToRealInverseFFTImageFilter<TInputImage, TOutputImage>
 {
 public:
   ITK_DISALLOW_COPY_AND_MOVE(VkHalfHermitianToRealInverseFFTImageFilter);
@@ -85,10 +85,25 @@ class VkHalfHermitianToRealInverseFFTImageFilter
 
   static constexpr unsigned int ImageDimension = InputImageType::ImageDimension;
 
-  /** Platform identifier for accelerator backend */
-  itkGetMacro(DeviceID, uint64_t);
+  /** Determine whether local or global properties will be
+   *  referenced for setting up GPU acceleration.
+   *  Defaults to global so that the user can adjust default properties
+   *  in filters constructed through the ITK object factory. */
+  itkSetMacro(UseVkGlobalConfiguration, bool);
+  itkGetMacro(UseVkGlobalConfiguration, bool);
+
+  /** Local platform identifier for accelerated backend.
+   *  Ignored if `UseVkGlobalConfiguration` is true. */
   itkSetMacro(DeviceID, uint64_t);
 
+  /** Return the enumerated GPU device to use for FFT
+   *  according to current filter settings. */
+  uint64_t
+  GetDeviceID() const
+  {
+    return m_UseVkGlobalConfiguration ? VkGlobalConfiguration::GetDeviceID() : m_DeviceID;
+  }
+
   SizeValueType
   GetSizeGreatestPrimeFactor() const override;
 
@@ -103,6 +118,7 @@ class VkHalfHermitianToRealInverseFFTImageFilter
   PrintSelf(std::ostream & os, Indent indent) const override;
 
 private:
+  bool     m_UseVkGlobalConfiguration{true};
   uint64_t m_DeviceID{ 0UL };
 
   VkCommon m_VkCommon{};
diff --git a/include/itkVkHalfHermitianToRealInverseFFTImageFilter.hxx b/include/itkVkHalfHermitianToRealInverseFFTImageFilter.hxx
index 77b8b73c..7a4f22a5 100644
--- a/include/itkVkHalfHermitianToRealInverseFFTImageFilter.hxx
+++ b/include/itkVkHalfHermitianToRealInverseFFTImageFilter.hxx
@@ -69,7 +69,7 @@ VkHalfHermitianToRealInverseFFTImageFilter<TInputImage, TOutputImage>::GenerateD
 
   // Mostly use defaults for VkCommon::VkGPU
   typename VkCommon::VkGPU vkGPU;
-  vkGPU.device_id = m_DeviceID;
+  vkGPU.device_id = this->GetDeviceID();
 
   // Describe this filter in VkCommon::VkParameters
   typename VkCommon::VkParameters vkParameters;
@@ -109,7 +109,10 @@ void
 VkHalfHermitianToRealInverseFFTImageFilter<TInputImage, TOutputImage>::PrintSelf(std::ostream & os, Indent indent) const
 {
   Superclass::PrintSelf(os, indent);
-  os << indent << "DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "UseVkGlobalConfiguration: " << m_UseVkGlobalConfiguration << std::endl;
+  os << indent << "Local DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "Global DeviceID: " << VkGlobalConfiguration::GetDeviceID() << std::endl;
+  os << indent << "Preferred DeviceID: " << this->GetDeviceID() << std::endl;
 }
 
 template <typename TInputImage, typename TOutputImage>
diff --git a/include/itkVkInverse1DFFTImageFilter.h b/include/itkVkInverse1DFFTImageFilter.h
index d80583f0..42fc8269 100644
--- a/include/itkVkInverse1DFFTImageFilter.h
+++ b/include/itkVkInverse1DFFTImageFilter.h
@@ -22,6 +22,7 @@
 #include "itkImage.h"
 #include "itkInverse1DFFTImageFilter.h"
 #include "itkVkCommon.h"
+#include "itkVkGlobalConfiguration.h"
 
 namespace itk
 {
@@ -34,9 +35,9 @@ namespace itk
  * implementation is based on the VkFFT library.
  *
  * This filter is multithreaded and by default supports input images with sizes which are
- * divisible by primes up to 13.
+ * divisible only by primes up to 13.
  *
- * Execution on input images with sizes divisible by primes greater than 17 may succeed
+ * Execution on input images with sizes divisible by primes greater than 13 may succeed
  * with a fallback on Bluestein's algorithm per VkFFT with a cost to performance and output precision.
  *
  * \ingroup FourierTransform
@@ -86,9 +87,25 @@ class VkInverse1DFFTImageFilter : public Inverse1DFFTImageFilter<TInputImage, TO
 
   static constexpr unsigned int ImageDimension = InputImageType::ImageDimension;
 
-  itkGetMacro(DeviceID, uint64_t);
+  /** Determine whether local or global properties will be
+   *  referenced for setting up GPU acceleration.
+   *  Defaults to global so that the user can adjust default properties
+   *  in filters constructed through the ITK object factory. */
+  itkSetMacro(UseVkGlobalConfiguration, bool);
+  itkGetMacro(UseVkGlobalConfiguration, bool);
+
+  /** Local platform identifier for accelerated backend.
+   *  Ignored if `UseVkGlobalConfiguration` is true. */
   itkSetMacro(DeviceID, uint64_t);
 
+  /** Return the enumerated GPU device to use for FFT
+   *  according to current filter settings. */
+  uint64_t
+  GetDeviceID() const
+  {
+    return m_UseVkGlobalConfiguration ? VkGlobalConfiguration::GetDeviceID() : m_DeviceID;
+  }
+
   SizeValueType
   GetSizeGreatestPrimeFactor() const override;
 
@@ -103,6 +120,7 @@ class VkInverse1DFFTImageFilter : public Inverse1DFFTImageFilter<TInputImage, TO
   PrintSelf(std::ostream & os, Indent indent) const override;
 
 private:
+  bool     m_UseVkGlobalConfiguration{true};
   uint64_t m_DeviceID{ 0UL };
 
   VkCommon m_VkCommon{};
diff --git a/include/itkVkInverse1DFFTImageFilter.hxx b/include/itkVkInverse1DFFTImageFilter.hxx
index 71d06cfd..9d1d8bbf 100644
--- a/include/itkVkInverse1DFFTImageFilter.hxx
+++ b/include/itkVkInverse1DFFTImageFilter.hxx
@@ -61,7 +61,7 @@ VkInverse1DFFTImageFilter<TInputImage, TOutputImage>::GenerateData()
 
   // Mostly use defaults for VkCommon::VkGPU
   typename VkCommon::VkGPU vkGPU;
-  vkGPU.device_id = m_DeviceID;
+  vkGPU.device_id = this->GetDeviceID();
 
   // Describe this filter in VkCommon::VkParameters
   typename VkCommon::VkParameters vkParameters;
@@ -108,7 +108,10 @@ void
 VkInverse1DFFTImageFilter<TInputImage, TOutputImage>::PrintSelf(std::ostream & os, Indent indent) const
 {
   Superclass::PrintSelf(os, indent);
-  os << indent << "DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "UseVkGlobalConfiguration: " << m_UseVkGlobalConfiguration << std::endl;
+  os << indent << "Local DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "Global DeviceID: " << VkGlobalConfiguration::GetDeviceID() << std::endl;
+  os << indent << "Preferred DeviceID: " << this->GetDeviceID() << std::endl;
 }
 
 template <typename TInputImage, typename TOutputImage>
diff --git a/include/itkVkInverseFFTImageFilter.h b/include/itkVkInverseFFTImageFilter.h
index 5b5a3ef4..dd52df9c 100644
--- a/include/itkVkInverseFFTImageFilter.h
+++ b/include/itkVkInverseFFTImageFilter.h
@@ -22,6 +22,7 @@
 #include "itkImage.h"
 #include "itkInverseFFTImageFilter.h"
 #include "itkVkCommon.h"
+#include "itkVkGlobalConfiguration.h"
 
 namespace itk
 {
@@ -34,7 +35,7 @@ namespace itk
  * implementation is based on the VkFFT library.
  *
  * This filter is multithreaded and supports input images with sizes which are
- * divisible by primes up to 13.
+ * divisible only by primes up to 13.
  *
  * \ingroup FourierTransform
  * \ingroup MultiThreaded
@@ -46,9 +47,7 @@ namespace itk
  */
 template <typename TInputImage,
           typename TOutputImage = Image<typename TInputImage::PixelType::value_type, TInputImage::ImageDimension>>
-    class VkInverseFFTImageFilter
-  : public InverseFFTImageFilter<TInputImage,TOutputImage>
-                                 
+class VkInverseFFTImageFilter : public InverseFFTImageFilter<TInputImage, TOutputImage>
 {
 public:
   ITK_DISALLOW_COPY_AND_MOVE(VkInverseFFTImageFilter);
@@ -85,9 +84,25 @@ template <typename TInputImage,
 
   static constexpr unsigned int ImageDimension = InputImageType::ImageDimension;
 
-  itkGetMacro(DeviceID, uint64_t);
+  /** Determine whether local or global properties will be
+   *  referenced for setting up GPU acceleration.
+   *  Defaults to global so that the user can adjust default properties
+   *  in filters constructed through the ITK object factory. */
+  itkSetMacro(UseVkGlobalConfiguration, bool);
+  itkGetMacro(UseVkGlobalConfiguration, bool);
+
+  /** Local platform identifier for accelerated backend.
+   *  Ignored if `UseVkGlobalConfiguration` is true. */
   itkSetMacro(DeviceID, uint64_t);
 
+  /** Return the enumerated GPU device to use for FFT
+   *  according to current filter settings. */
+  uint64_t
+  GetDeviceID() const
+  {
+    return m_UseVkGlobalConfiguration ? VkGlobalConfiguration::GetDeviceID() : m_DeviceID;
+  }
+
   SizeValueType
   GetSizeGreatestPrimeFactor() const override;
 
@@ -102,6 +117,7 @@ template <typename TInputImage,
   PrintSelf(std::ostream & os, Indent indent) const override;
 
 private:
+  bool     m_UseVkGlobalConfiguration{true};
   uint64_t m_DeviceID{ 0UL };
 
   VkCommon m_VkCommon{};
diff --git a/include/itkVkInverseFFTImageFilter.hxx b/include/itkVkInverseFFTImageFilter.hxx
index 9c1d47fe..ba1d1ade 100644
--- a/include/itkVkInverseFFTImageFilter.hxx
+++ b/include/itkVkInverseFFTImageFilter.hxx
@@ -31,12 +31,12 @@ namespace itk
 {
 
 template <typename TInputImage, typename TOutputImage>
-VkInverseFFTImageFilter<TInputImage,TOutputImage>::VkInverseFFTImageFilter()
+VkInverseFFTImageFilter<TInputImage, TOutputImage>::VkInverseFFTImageFilter()
 {}
 
 template <typename TInputImage, typename TOutputImage>
 void
-VkInverseFFTImageFilter<TInputImage,TOutputImage>::GenerateData()
+VkInverseFFTImageFilter<TInputImage, TOutputImage>::GenerateData()
 {
   // get pointers to the input and output
   const InputImageType * const input{ this->GetInput() };
@@ -66,7 +66,7 @@ VkInverseFFTImageFilter<TInputImage,TOutputImage>::GenerateData()
 
   // Mostly use defaults for VkCommon::VkGPU
   typename VkCommon::VkGPU vkGPU;
-  vkGPU.device_id = m_DeviceID;
+  vkGPU.device_id = this->GetDeviceID();
 
   // Describe this filter in VkCommon::VkParameters
   typename VkCommon::VkParameters vkParameters;
@@ -103,15 +103,18 @@ VkInverseFFTImageFilter<TInputImage,TOutputImage>::GenerateData()
 
 template <typename TInputImage, typename TOutputImage>
 void
-VkInverseFFTImageFilter<TInputImage,TOutputImage>::PrintSelf(std::ostream & os, Indent indent) const
+VkInverseFFTImageFilter<TInputImage, TOutputImage>::PrintSelf(std::ostream & os, Indent indent) const
 {
   Superclass::PrintSelf(os, indent);
-  os << indent << "DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "UseVkGlobalConfiguration: " << m_UseVkGlobalConfiguration << std::endl;
+  os << indent << "Local DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "Global DeviceID: " << VkGlobalConfiguration::GetDeviceID() << std::endl;
+  os << indent << "Preferred DeviceID: " << this->GetDeviceID() << std::endl;
 }
 
 template <typename TInputImage, typename TOutputImage>
-typename VkInverseFFTImageFilter<TInputImage,TOutputImage>::SizeValueType
-VkInverseFFTImageFilter<TInputImage,TOutputImage>::GetSizeGreatestPrimeFactor() const
+typename VkInverseFFTImageFilter<TInputImage, TOutputImage>::SizeValueType
+VkInverseFFTImageFilter<TInputImage, TOutputImage>::GetSizeGreatestPrimeFactor() const
 {
   return m_VkCommon.GetGreatestPrimeFactor();
 }
diff --git a/include/itkVkRealToHalfHermitianForwardFFTImageFilter.h b/include/itkVkRealToHalfHermitianForwardFFTImageFilter.h
index 76882223..d59f0da4 100644
--- a/include/itkVkRealToHalfHermitianForwardFFTImageFilter.h
+++ b/include/itkVkRealToHalfHermitianForwardFFTImageFilter.h
@@ -22,6 +22,7 @@
 #include "itkImage.h"
 #include "itkRealToHalfHermitianForwardFFTImageFilter.h"
 #include "itkVkCommon.h"
+#include "itkVkGlobalConfiguration.h"
 
 namespace itk
 {
@@ -34,7 +35,7 @@ namespace itk
  * implementation is based on the VkFFT library.
  *
  * This filter is multithreaded and supports input images with sizes which are
- * divisible by primes up to 13.
+ * divisible only by primes up to 13.
  *
  * \ingroup FourierTransform
  * \ingroup MultiThreaded
@@ -84,9 +85,25 @@ class VkRealToHalfHermitianForwardFFTImageFilter
 
   static constexpr unsigned int ImageDimension = InputImageType::ImageDimension;
 
-  itkGetMacro(DeviceID, uint64_t);
+  /** Determine whether local or global properties will be
+   *  referenced for setting up GPU acceleration.
+   *  Defaults to global so that the user can adjust default properties
+   *  in filters constructed through the ITK object factory. */
+  itkSetMacro(UseVkGlobalConfiguration, bool);
+  itkGetMacro(UseVkGlobalConfiguration, bool);
+
+  /** Local platform identifier for accelerated backend.
+   *  Ignored if `UseVkGlobalConfiguration` is true. */
   itkSetMacro(DeviceID, uint64_t);
 
+  /** Return the enumerated GPU device to use for FFT
+   *  according to current filter settings. */
+  uint64_t
+  GetDeviceID() const
+  {
+    return m_UseVkGlobalConfiguration ? VkGlobalConfiguration::GetDeviceID() : m_DeviceID;
+  }
+
   SizeValueType
   GetSizeGreatestPrimeFactor() const override;
 
@@ -101,6 +118,7 @@ class VkRealToHalfHermitianForwardFFTImageFilter
   PrintSelf(std::ostream & os, Indent indent) const override;
 
 private:
+  bool     m_UseVkGlobalConfiguration{true};
   uint64_t m_DeviceID{ 0UL };
 
   VkCommon m_VkCommon{};
diff --git a/include/itkVkRealToHalfHermitianForwardFFTImageFilter.hxx b/include/itkVkRealToHalfHermitianForwardFFTImageFilter.hxx
index b8cd018d..5d2bbe4e 100644
--- a/include/itkVkRealToHalfHermitianForwardFFTImageFilter.hxx
+++ b/include/itkVkRealToHalfHermitianForwardFFTImageFilter.hxx
@@ -66,7 +66,7 @@ VkRealToHalfHermitianForwardFFTImageFilter<TInputImage, TOutputImage>::GenerateD
 
   // Mostly use defaults for VkCommon::VkGPU
   typename VkCommon::VkGPU vkGPU;
-  vkGPU.device_id = m_DeviceID;
+  vkGPU.device_id = this->GetDeviceID();
 
   // Describe this filter in VkCommon::VkParameters
   typename VkCommon::VkParameters vkParameters;
@@ -106,7 +106,10 @@ void
 VkRealToHalfHermitianForwardFFTImageFilter<TInputImage, TOutputImage>::PrintSelf(std::ostream & os, Indent indent) const
 {
   Superclass::PrintSelf(os, indent);
-  os << indent << "DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "UseVkGlobalConfiguration: " << m_UseVkGlobalConfiguration << std::endl;
+  os << indent << "Local DeviceID: " << m_DeviceID << std::endl;
+  os << indent << "Global DeviceID: " << VkGlobalConfiguration::GetDeviceID() << std::endl;
+  os << indent << "Preferred DeviceID: " << this->GetDeviceID() << std::endl;
 }
 
 template <typename TInputImage, typename TOutputImage>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 17366292..1eac757c 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,5 +1,6 @@
 set(VkFFTBackend_SRCS
   itkVkCommon.cxx
+  itkVkGlobalConfiguration.cxx
   )
 
 itk_module_add_library(VkFFTBackend ${VkFFTBackend_SRCS})
diff --git a/src/itkVkGlobalConfiguration.cxx b/src/itkVkGlobalConfiguration.cxx
new file mode 100644
index 00000000..9fbf0b00
--- /dev/null
+++ b/src/itkVkGlobalConfiguration.cxx
@@ -0,0 +1,77 @@
+/*=========================================================================
+ *
+ *  Copyright NumFOCUS
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0.txt
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *=========================================================================*/
+#include "itkVkGlobalConfiguration.h"
+
+#include <mutex>
+#include "itkSingleton.h"
+
+namespace itk
+{
+struct VkGlobalConfigurationGlobals
+{
+  VkGlobalConfiguration::Pointer m_Instance{nullptr};
+  std::mutex                     m_CreationLock;
+};
+
+itkGetGlobalSimpleMacro(VkGlobalConfiguration, VkGlobalConfigurationGlobals, PimplGlobals);
+
+VkGlobalConfigurationGlobals * VkGlobalConfiguration::m_PimplGlobals;
+
+VkGlobalConfiguration::Pointer
+VkGlobalConfiguration::GetInstance()
+{
+  itkInitGlobalsMacro(PimplGlobals);
+  if (!m_PimplGlobals->m_Instance)
+  {
+    m_PimplGlobals->m_CreationLock.lock();
+    // Need to make sure that during gaining access
+    // to the lock that some other thread did not
+    // initialize the singleton.
+    if (!m_PimplGlobals->m_Instance)
+    {
+      m_PimplGlobals->m_Instance = Self::New();
+      if (!m_PimplGlobals->m_Instance)
+      {
+        std::ostringstream message;
+        message << "itk::ERROR: "
+                << "VkGlobalConfiguration"
+                << " Valid VkGlobalConfiguration instance not created";
+        itk::ExceptionObject e_(__FILE__, __LINE__, message.str().c_str(), ITK_LOCATION);
+        throw e_; /* Explicit naming to work around Intel compiler bug.  */
+      }
+    }
+    m_PimplGlobals->m_CreationLock.unlock();
+  }
+  return m_PimplGlobals->m_Instance;
+}
+
+void
+VkGlobalConfiguration::SetDeviceID(const uint64_t id)
+{
+  itkInitGlobalsMacro(PimplGlobals);
+  GetInstance()->m_DeviceID = id;
+}
+
+uint64_t
+VkGlobalConfiguration::GetDeviceID()
+{
+  itkInitGlobalsMacro(PimplGlobals);
+  return GetInstance()->m_DeviceID;
+}
+
+} // namespace itk
diff --git a/wrapping/itkVkGlobalConfiguration.wrap b/wrapping/itkVkGlobalConfiguration.wrap
new file mode 100644
index 00000000..b81cbc85
--- /dev/null
+++ b/wrapping/itkVkGlobalConfiguration.wrap
@@ -0,0 +1,1 @@
+itk_wrap_simple_class("itk::VkGlobalConfiguration" POINTER)


Test patch:
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 960d069f..a6eab699 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -8,6 +8,7 @@ set(VkFFTBackendTests
   itkVkForwardInverseFFTImageFilterTest.cxx
   itkVkForwardInverse1DFFTImageFilterTest.cxx
   itkVkForward1DFFTImageFilterBaselineTest.cxx
+  itkVkGlobalConfigurationTest.cxx
   itkVkHalfHermitianFFTImageFilterTest.cxx
   itkVkInverse1DFFTImageFilterBaselineTest.cxx
   )
@@ -88,3 +89,7 @@ itk_add_test(NAME itkVkFFTImageFilterFactoryTest
   COMMAND VkFFTBackendTestDriver
   itkVkFFTImageFilterFactoryTest
    )
+
+itk_add_test(NAME itkVkGlobalConfigurationTest
+  COMMAND VkFFTBackendTestDriver
+  itkVkGlobalConfigurationTest)
diff --git a/test/itkVkComplexToComplex1DFFTImageFilterSizesTest.cxx b/test/itkVkComplexToComplex1DFFTImageFilterSizesTest.cxx
index 9ed3208d..321c162e 100644
--- a/test/itkVkComplexToComplex1DFFTImageFilterSizesTest.cxx
+++ b/test/itkVkComplexToComplex1DFFTImageFilterSizesTest.cxx
@@ -23,6 +23,7 @@
 #include "itkCommand.h"
 #include "itkImageFileWriter.h"
 #include "itkTestingMacros.h"
+#include "itkVkGlobalConfiguration.h"
 
 namespace
 {
@@ -75,7 +76,9 @@ itkVkComplexToComplex1DFFTImageFilterSizesTest(int argc, char * argv[])
 
     using FilterType = itk::VkComplexToComplex1DFFTImageFilter<ComplexImageType>;
     typename FilterType::Pointer filter{ FilterType::New() };
-    filter->SetDeviceID(0);
+    //filter->SetDeviceID(0);
+    itk::VkGlobalConfiguration::SetDeviceID(0);
+
 
     ITK_EXERCISE_BASIC_OBJECT_METHODS(filter, VkComplexToComplex1DFFTImageFilter, ComplexToComplex1DFFTImageFilter);
 
@@ -136,7 +139,7 @@ itkVkComplexToComplex1DFFTImageFilterSizesTest(int argc, char * argv[])
 
       using FilterType = itk::VkComplexToComplex1DFFTImageFilter<ComplexImageType>;
       typename FilterType::Pointer filter{ FilterType::New() };
-      filter->SetDeviceID(0);
+      //filter->SetDeviceID(0);
       filter->SetInput(image);
 
       ITK_TRY_EXPECT_NO_EXCEPTION(filter->Update());
diff --git a/test/itkVkGlobalConfigurationTest.cxx b/test/itkVkGlobalConfigurationTest.cxx
new file mode 100644
index 00000000..04e7a3ad
--- /dev/null
+++ b/test/itkVkGlobalConfigurationTest.cxx
@@ -0,0 +1,84 @@
+/*=========================================================================
+ *
+ *  Copyright NumFOCUS
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0.txt
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *=========================================================================*/
+
+#include <complex>
+
+#include "itkVkComplexToComplex1DFFTImageFilter.h"
+#include "itkVkComplexToComplexFFTImageFilter.h"
+#include "itkVkForward1DFFTImageFilter.h"
+#include "itkVkForwardFFTImageFilter.h"
+#include "itkVkHalfHermitianToRealInverseFFTImageFilter.h"
+#include "itkVkInverse1DFFTImageFilter.h"
+#include "itkVkInverseFFTImageFilter.h"
+#include "itkVkRealToHalfHermitianForwardFFTImageFilter.h"
+
+#include "itkVkGlobalConfiguration.h"
+#include "itkTestingMacros.h"
+
+// Verify FFT interface classes can be instantiated with
+// VkFFT backends through ITK object factory override methods
+
+template <typename FFTImageFilterType>
+int
+itkVkGlobalConfigurationTestProcedure()
+{
+  // Verify that we can set global configuration properties
+  itk::VkGlobalConfiguration::SetDeviceID(1);
+  ITK_TEST_SET_GET_VALUE(itk::VkGlobalConfiguration::GetDeviceID(), 1);
+
+  // Verify global configuration properties are picked up by filters by default
+
+  auto fftFilter = FFTImageFilterType::New();
+  ITK_TEST_SET_GET_VALUE(fftFilter->GetUseVkGlobalConfiguration(), true);
+  ITK_TEST_SET_GET_VALUE(fftFilter->GetDeviceID(), 1);
+  fftFilter->SetDeviceID(2);
+  ITK_TEST_SET_GET_VALUE(fftFilter->GetDeviceID(), 1);
+  itk::VkGlobalConfiguration::SetDeviceID(0);
+  ITK_TEST_SET_GET_VALUE(fftFilter->GetDeviceID(), 0);
+
+  // Verify global configuration can be ignored via filter settings
+  fftFilter->SetUseVkGlobalConfiguration(false);
+  ITK_TEST_SET_GET_VALUE(fftFilter->GetUseVkGlobalConfiguration(), false);
+  ITK_TEST_SET_GET_VALUE(fftFilter->GetDeviceID(), 2);
+  itk::VkGlobalConfiguration::SetDeviceID(0);
+  ITK_TEST_SET_GET_VALUE(fftFilter->GetDeviceID(), 2);
+  fftFilter->SetDeviceID(1);
+  ITK_TEST_SET_GET_VALUE(fftFilter->GetDeviceID(), 1);
+
+  return EXIT_SUCCESS;
+}
+
+int
+itkVkGlobalConfigurationTest(int argc, char * argv[])
+{
+  using RealImageType = itk::Image<float, 2>;
+  using ComplexImageType = itk::Image<std::complex<float>, 2>;
+
+  itkVkGlobalConfigurationTestProcedure<itk::VkComplexToComplex1DFFTImageFilter<ComplexImageType, ComplexImageType>>();
+  itkVkGlobalConfigurationTestProcedure<itk::VkComplexToComplexFFTImageFilter<ComplexImageType, ComplexImageType>>();
+  itkVkGlobalConfigurationTestProcedure<itk::VkForward1DFFTImageFilter<RealImageType, ComplexImageType>>();
+  itkVkGlobalConfigurationTestProcedure<itk::VkForwardFFTImageFilter<RealImageType, ComplexImageType>>();
+  itkVkGlobalConfigurationTestProcedure<
+    itk::VkHalfHermitianToRealInverseFFTImageFilter<ComplexImageType, RealImageType>>();
+  itkVkGlobalConfigurationTestProcedure<itk::VkInverse1DFFTImageFilter<ComplexImageType, RealImageType>>();
+  itkVkGlobalConfigurationTestProcedure<itk::VkInverseFFTImageFilter<ComplexImageType, RealImageType>>();
+  itkVkGlobalConfigurationTestProcedure<
+    itk::VkRealToHalfHermitianForwardFFTImageFilter<RealImageType, ComplexImageType>>();
+
+  return EXIT_SUCCESS;
+}