diff --git a/setup.py b/setup.py
index 8f48e5f07b58589abbadbf374a59425314df229e..09e85066997bff1c93ece7e7feecfb52d5157f6c 100644
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@ setuptools.setup(name='pairs',
     install_requires=[],
     packages=['pairs'] + [f"pairs.{mod}" for mod in modules],
     package_dir={'pairs': 'src/pairs'},
-    package_data={'pairs.runtime': ['runtime/*.hpp']},
+    package_data={'pairs': ['runtime/*.hpp']},
     classifiers=[
         "Programming Language :: Python :: 3",
         "License :: OSI Approved :: MIT License",
diff --git a/src/pairs/code_gen/cgen.py b/src/pairs/code_gen/cgen.py
index 219e9e950597390529636c53f7eaaed917d281e0..850d74c83ae9eabeb70edd2ecbe75a4726f63e0b 100644
--- a/src/pairs/code_gen/cgen.py
+++ b/src/pairs/code_gen/cgen.py
@@ -5,6 +5,7 @@ from pairs.ir.branches import Branch
 from pairs.ir.cast import Cast
 from pairs.ir.bin_op import BinOp, Decl, VectorAccess
 from pairs.ir.data_types import Type_Int, Type_Float, Type_String, Type_Vector
+from pairs.ir.device import DeviceCopy
 from pairs.ir.functions import Call
 from pairs.ir.layouts import Layout_AoS, Layout_SoA, Layout_Invalid
 from pairs.ir.lit import Lit
@@ -130,6 +131,9 @@ class CGen:
             call = self.generate_expression(ast_node)
             self.print(f"{call};")
 
+        if isinstance(ast_node, DeviceCopy):
+            self.print(f"pairs::copy_to_device({ast_node.prop.name()})")
+
         if isinstance(ast_node, For):
             iterator = self.generate_expression(ast_node.iterator)
             lower_range = None
diff --git a/src/pairs/ir/block.py b/src/pairs/ir/block.py
index 853de45289b9680abe943d5619fa4f45d3611bc8..cc3c50c7abfbe76c5b3f465e4c7bb552a97d82dc 100644
--- a/src/pairs/ir/block.py
+++ b/src/pairs/ir/block.py
@@ -4,31 +4,13 @@ from pairs.ir.ast_node import ASTNode
 class Block(ASTNode):
     def __init__(self, sim, stmts):
         super().__init__(sim)
-        self.level = 0
         self.variants = set()
-        self.props_accessed = {}
-        self.props_to_sync = set()
 
         if isinstance(stmts, Block):
             self.stmts = stmts.statements()
         else:
             self.stmts = [stmts] if not isinstance(stmts, list) else stmts
 
-    def __lt__(self, other):
-        return self.level < other.level
-
-    def __le__(self, other):
-        return self.level <= other.level
-
-    def __gt__(self, other):
-        return self.level > other.level
-
-    def __ge__(self, other):
-        return self.level >= other.level
-
-    def set_level(self, level):
-        self.level = level
-
     def add_statement(self, stmt):
         if isinstance(stmt, list):
             self.stmts = self.stmts + stmt
@@ -59,3 +41,24 @@ class Block(ASTNode):
             result_block = Block.merge_blocks(result_block, block)
 
         return result_block
+
+
+class KernelBlock(Block):
+    def __init__(self, sim, stmts, run_on_host=False):
+        super().__init__(sim, stmts)
+        self.run_on_host = run_on_host
+        self.props_accessed = {}
+
+    def add_property_access(self, prop, oper):
+        prop_key = prop.name()
+        if prop_key not in self.props_accessed:
+            self.props_accessed[prop_key] = oper
+
+        elif oper not in self.props_accessed[prop_key]:
+            self.props_accessed[prop_key] += oper
+
+    def properties_to_synchronize(self):
+        return {p for p in self.props_accessed if self.props_accessed[p][0] == 'r'}
+
+    def writing_properties(self):
+        return {p for p in self.props_accessed if 'w' in self.props_accessed[p][0]}
diff --git a/src/pairs/ir/device.py b/src/pairs/ir/device.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9c8903b5ffee7d0bcb5ffe4a207d5d2a67445d2
--- /dev/null
+++ b/src/pairs/ir/device.py
@@ -0,0 +1,10 @@
+from pairs.ir.ast_node import ASTNode
+
+
+class DeviceCopy(ASTNode):
+    def __init__(self, sim, prop):
+        super().__init__(sim)
+        self.prop = prop
+
+    def children(self):
+        return [self.prop]
diff --git a/src/pairs/sim/kernel_wrapper.py b/src/pairs/sim/kernel_wrapper.py
index ccf7f5adfbe3568048590616af819fc05af77530..53cd870f85f8f44491b6231856d34795e337640b 100644
--- a/src/pairs/sim/kernel_wrapper.py
+++ b/src/pairs/sim/kernel_wrapper.py
@@ -1,12 +1,13 @@
-from pairs.ir.block import Block
+from pairs.ir.block import Block, KernelBlock
 
 
 class KernelWrapper():
-    def __init__(self):
-        self.kernels = Block(self, [])
+    def __init__(self, sim):
+        self.sim = sim
+        self.kernels = Block(sim, [])
 
     def add_kernel_block(self, block):
-        self.kernels = Block.merge_blocks(self.kernels, block)
+        self.kernels = Block.merge_blocks(self.kernels, KernelBlock(self.sim, block))
 
     def lower(self):
         return self.kernels
diff --git a/src/pairs/sim/particle_simulation.py b/src/pairs/sim/particle_simulation.py
index 2eb5c89d455f4a80f3d2fb46e369d3783b223af0..1456f4165d74fb14ea101557b27d76ae62bb3c20 100644
--- a/src/pairs/sim/particle_simulation.py
+++ b/src/pairs/sim/particle_simulation.py
@@ -21,6 +21,7 @@ from pairs.sim.setup_wrapper import SetupWrapper
 from pairs.sim.timestep import Timestep
 from pairs.sim.variables import VariablesDecl
 from pairs.sim.vtk import VTKWrite
+from pairs.transformations.add_device_copies import add_device_copies
 from pairs.transformations.prioritize_scalar_ops import prioritaze_scalar_ops
 from pairs.transformations.set_used_bin_ops import set_used_bin_ops
 from pairs.transformations.simplify import simplify_expressions
@@ -47,8 +48,8 @@ class ParticleSimulation:
         self.nest = False
         self.check_decl_usage = True
         self.block = Block(self, [])
-        self.setups = SetupWrapper()
-        self.kernels = KernelWrapper()
+        self.setups = SetupWrapper(self)
+        self.kernels = KernelWrapper(self)
         self.dims = dims
         self.ntimesteps = timesteps
         self.expr_id = 0
@@ -218,6 +219,7 @@ class ParticleSimulation:
         simplify_expressions(program)
         move_loop_invariant_code(program)
         set_used_bin_ops(program)
+        add_device_copies(program)
 
         # For this part on, all bin ops are generated without usage verification
         self.check_decl_usage = False
diff --git a/src/pairs/sim/setup_wrapper.py b/src/pairs/sim/setup_wrapper.py
index 629766722bb5546eda98a917b7c6487d048943a0..70b7d746b573d637d7cd63fdf242d055fab4db22 100644
--- a/src/pairs/sim/setup_wrapper.py
+++ b/src/pairs/sim/setup_wrapper.py
@@ -2,8 +2,8 @@ from pairs.ir.block import Block
 
 
 class SetupWrapper():
-    def __init__(self):
-        self.setups = Block(self, [])
+    def __init__(self, sim):
+        self.setups = Block(sim, [])
 
     def add_setup_block(self, block):
         self.setups = Block.merge_blocks(self.setups, block)
diff --git a/src/pairs/transformations/add_device_copies.py b/src/pairs/transformations/add_device_copies.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8d431fa582281b4d1fe93855c76390cda550c66
--- /dev/null
+++ b/src/pairs/transformations/add_device_copies.py
@@ -0,0 +1,64 @@
+from pairs.ir.block import KernelBlock
+from pairs.ir.device import DeviceCopy
+from pairs.ir.mutator import Mutator
+from pairs.ir.visitor import Visitor
+
+
+class AddAccessedProperties(Visitor):
+    def __init__(self, ast):
+        super().__init__(ast)
+        self.current_kernel_block = None
+        self.writing = False
+
+    def visit_Assign(self, ast_node):
+        for s in ast_node.sources():
+            self.visit(s)
+        self.writing = True
+
+        for d in ast_node.destinations():
+            self.visit(d)
+        self.writing = False
+
+    def visit_KernelBlock(self, ast_node):
+        self.current_kernel_block = ast_node
+        self.visit_children(ast_node)
+
+    def visit_PropertyAccess(self, ast_node):
+        if self.current_kernel_block is not None:
+            self.current_kernel_block.add_property_access(ast_node.prop, 'w' if self.writing else 'r')
+
+
+class AddDeviceCopies(Mutator):
+    def __init__(self, ast):
+        super().__init__(ast)
+        self.synchronized_props = set()
+        self.props_to_copy = {}
+
+    def mutate_Block(self, ast_node):
+        new_stmts = []
+        stmts = [self.mutate(s) for s in ast_node.stmts]
+
+        for s in stmts:
+            if s is not None:
+                s_id = id(s)
+                if isinstance(s, KernelBlock) and s_id in self.props_to_copy:
+                    for p in self.props.to_copy[s_id]:
+                        new_stmts = new_stmts + DeviceCopy(ast_node.sim, p)
+
+                new_stmts.append(s)
+
+        ast_node.stmts = new_stmts
+        return ast_node
+
+    def mutate_KernelBlock(self, ast_node):
+        copying_properties = {p for p in ast_node.properties_to_synchronize() if p not in synchronized_props}
+        self.props_to_copy[id(ast_node)] = copying_properties
+        self.synchronized_props.update(copying_properties)
+        self.synchronized_props -= ast_node.writing_properties()
+
+
+def add_device_copies(ast):
+    add_accessed_props = AddAccessedProperties(ast)
+    add_accessed_props.visit()
+    add_copies = AddDeviceCopies(ast)
+    add_copies.mutate()