diff --git a/fffw/encoding/filters.py b/fffw/encoding/filters.py
index 43ed444..2564388 100644
--- a/fffw/encoding/filters.py
+++ b/fffw/encoding/filters.py
@@ -157,6 +157,7 @@ class Scale(VideoFilter):
     :arg height: resulting video height
     """
     filter = "scale"
+    hardware = None  # cpu only
 
     width: int = param(name='w')
     height: int = param(name='h')
@@ -202,10 +203,6 @@ class Split(AutoFilter):
             return ''
         return str(self.output_count)
 
-    def validate_edge_device(self, edge: base.Edge) -> None:
-        # Any device is supported
-        return
-
 
 @dataclass
 class Trim(AutoFilter):
@@ -355,10 +352,6 @@ class Concat(Filter):
             kwargs['samples'] = samples
         return replace(metadata[0], **kwargs)
 
-    def validate_edge_device(self, edge: base.Edge) -> None:
-        # Any device is supported
-        return
-
 
 @dataclass
 class Overlay(VideoFilter):
diff --git a/fffw/encoding/mixins.py b/fffw/encoding/mixins.py
index e74310c..a0e4a2d 100644
--- a/fffw/encoding/mixins.py
+++ b/fffw/encoding/mixins.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional
 
 from fffw.graph import base, VIDEO
 
@@ -9,7 +9,7 @@ else:
 
 
 class StreamValidationMixin(StreamValidationTarget):
-    hardware: str
+    hardware: Optional[str]
 
     def connect_edge(self, edge: base.Edge) -> base.Edge:
         self.validate_edge_kind(edge)
@@ -30,7 +30,11 @@ class StreamValidationMixin(StreamValidationTarget):
         meta = edge.get_meta_data(self)
         if meta is None:
             return
-        filter_hardware = getattr(self, 'hardware', None)
+        try:
+            filter_hardware = getattr(self, 'hardware')
+        except AttributeError:
+            # no hardware restrictions for filter/codec
+            return
         device = getattr(meta, 'device', None)
         edge_hardware = None if device is None else device.hardware
         if filter_hardware != edge_hardware:

diff --git a/tests/test_encoding.py b/tests/test_encoding.py
index 005be16..959746a 100644
--- a/tests/test_encoding.py
+++ b/tests/test_encoding.py
@@ -1,3 +1,4 @@
+from dataclasses import dataclass
 from unittest import TestCase
 
 from fffw.graph import StreamType, VIDEO, AUDIO, video_meta_data
@@ -81,8 +82,13 @@ class InputsTestCase(TestCase):
                            hardware='cuda',
                            device='foo')
 
+        @dataclass
+        class X264(VideoCodec):
+            codec = 'libx264'
+            hardware = None  # cpu only
+
         with self.assertRaises(ValueError):
-            src.video > VideoCodec('libx264')
+            src.video > X264()
 
         with self.assertRaises(ValueError):
             src.video | filters.Scale(640, 360)
diff --git a/tests/test_graph.py b/tests/test_graph.py
index 6b9bb4c..dd68ec9 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -306,16 +306,20 @@ class FilterGraphTestCase(TestCase):
         cuda = meta.Device(hardware='cuda', name='foo')
         self.source.video | Upload(device=cuda) | ScaleCuda(640, 360)
 
-    def test_concat_split_allows_any_hardware(self):
+    def test_any_hardware_filter(self):
         """
-        Concat and split filters allow any hardware acceleration.
+        A filter may be defined that allows to be ran on any hardware
         """
+
+        @dataclass
+        class UniversalFilter(VideoFilter):
+            filter = 'filter'
+            # not setting hardware - universal filter
+
         try:
             cuda = meta.Device(hardware='cuda', name='foo')
-            hw = self.source.video | Upload(device=cuda)
-            split = hw | Split(VIDEO, output_count=2)
-            concat = Concat(VIDEO, input_count=2)
-            split | concat
-            split | concat
+            s = self.source.video | Split(VIDEO)
+            s | UniversalFilter()
+            s | Upload(device=cuda) | UniversalFilter()
         except ValueError:  # pragma: no cover
             self.fail("hardware validation unexpectedly failed")

Split and concat should validate against any hardware
splitting and concatenation is supported in every hardware acceleration
tests/test_graph.py::FilterGraphTestCase::test_any_hardware_filter
tests/test_encoding.py::InputsTestCase::test_append_source tests/test_encoding.py::InputsTestCase::test_default_input tests/test_encoding.py::InputsTestCase::test_input_list tests/test_encoding.py::InputsTestCase::test_validate_input_hardware tests/test_encoding.py::InputsTestCase::test_validate_stream_kind tests/test_encoding.py::OutputsTestCase::test_codec_validates_hardware_device tests/test_encoding.py::OutputsTestCase::test_codec_validates_stream_kind tests/test_graph.py::FilterGraphTestCase::test_concat_audio_metadata tests/test_graph.py::FilterGraphTestCase::test_concat_metadata tests/test_graph.py::FilterGraphTestCase::test_disabled_filters tests/test_graph.py::FilterGraphTestCase::test_filter_graph tests/test_graph.py::FilterGraphTestCase::test_filter_validates_hardware_device tests/test_graph.py::FilterGraphTestCase::test_filter_validates_stream_kind tests/test_graph.py::FilterGraphTestCase::test_overlay_metadata tests/test_graph.py::FilterGraphTestCase::test_scale_changes_metadata tests/test_graph.py::FilterGraphTestCase::test_setpts_metadata tests/test_graph.py::FilterGraphTestCase::test_skip_not_connected_sources tests/test_graph.py::FilterGraphTestCase::test_trim_metadata