diff --git a/src/CodeGen_Internal.cpp b/src/CodeGen_Internal.cpp
index 5c36ea58aae3..2fc5b5cae0df 100644
--- a/src/CodeGen_Internal.cpp
+++ b/src/CodeGen_Internal.cpp
@@ -40,6 +40,7 @@ bool function_takes_user_context(const std::string &name) {
         "halide_device_malloc",
         "halide_device_and_host_malloc",
         "halide_device_sync",
+        "halide_device_sync_global",
         "halide_do_par_for",
         "halide_do_loop_task",
         "halide_do_task",
diff --git a/src/OffloadGPULoops.cpp b/src/OffloadGPULoops.cpp
index 7b8464211994..46e6544036b7 100644
--- a/src/OffloadGPULoops.cpp
+++ b/src/OffloadGPULoops.cpp
@@ -249,7 +249,13 @@ class InjectGpuOffload : public IRMutator {
             Call::make(Handle(), Call::make_struct, args, Call::Intrinsic),
             Call::make(Handle(), Call::make_struct, arg_is_buffer, Call::Intrinsic),
         };
-        return call_extern_and_assert("halide_" + api_unique_name + "_run", run_args);
+        Stmt run_and_assert = call_extern_and_assert("halide_" + api_unique_name + "_run", run_args);
+        if (target.has_feature(Target::Profile) || target.has_feature(Target::ProfileByTimer)) {
+            Expr device_interface = make_device_interface_call(loop->device_api, MemoryType::Auto);
+            Stmt sync_and_assert = call_extern_and_assert("halide_device_sync_global", {device_interface});
+            return Block::make(run_and_assert, sync_and_assert);
+        }
+        return run_and_assert;
     }
 
 public:
diff --git a/src/Profiling.cpp b/src/Profiling.cpp
index 7bd9a9fe4db7..2be058b3c8a6 100644
--- a/src/Profiling.cpp
+++ b/src/Profiling.cpp
@@ -6,6 +6,7 @@
 #include "ExprUsesVar.h"
 #include "IRMutator.h"
 #include "IROperator.h"
+#include "InjectHostDevBufferCopies.h"
 #include "Profiling.h"
 #include "Scope.h"
 #include "Simplify.h"
@@ -422,6 +423,77 @@ class InjectProfiling : public IRMutator {
         }
         return IfThenElse::make(std::move(condition), std::move(then_case), std::move(else_case));
     }
+
+    Stmt visit(const LetStmt *op) override {
+        if (const Call *call = op->value.as<Call>()) {
+            Stmt start_profiler;
+            if (call->name == "halide_copy_to_host" || call->name == "halide_copy_to_device") {
+                std::string buffer_name;
+                if (const Variable *var = call->args.front().as<Variable>()) {
+                    buffer_name = var->name;
+                    if (ends_with(buffer_name, ".buffer")) {
+                        buffer_name = buffer_name.substr(0, buffer_name.size() - 7);
+                    } else {
+                        internal_error << "Expected to find a variable ending in .buffer as first argument to function call " << call->name << "\n";
+                    }
+                } else {
+                    internal_error << "Expected to find a variable as first argument of the function call " << call->name << ".\n";
+                }
+                bool requires_sync = false;
+                if (call->name == "halide_copy_to_host") {
+                    int copy_to_host_id = get_func_id(buffer_name + " (copy to host)");
+                    start_profiler = set_current_func(copy_to_host_id);
+                    requires_sync = false;
+                } else if (call->name == "halide_copy_to_device") {
+                    int copy_to_device_id = get_func_id(buffer_name + " (copy to device)");
+                    start_profiler = set_current_func(copy_to_device_id);
+                    requires_sync = true;
+                } else {
+                    internal_error << "Unexpected function name.\n";
+                }
+                if (start_profiler.defined()) {
+                    // The copy functions are followed by an assert, which we will wrap in the timed body.
+                    const AssertStmt *copy_assert = nullptr;
+                    Stmt other;
+                    if (const Block *block = op->body.as<Block>()) {
+                        if (const AssertStmt *assert = block->first.as<AssertStmt>()) {
+                            copy_assert = assert;
+                            other = block->rest;
+                        }
+                    } else if (const AssertStmt *assert = op->body.as<AssertStmt>()) {
+                        copy_assert = assert;
+                    }
+                    if (copy_assert) {
+                        std::vector<Stmt> steps;
+                        steps.push_back(AssertStmt::make(copy_assert->condition, copy_assert->message));
+                        if (requires_sync) {
+                            internal_assert(call->name == "halide_copy_to_device");
+                            Expr device_interface = call->args.back();  // The last argument to the copy_to_device calls is the device_interface.
+                            Stmt sync_and_assert = call_extern_and_assert("halide_device_sync_global", {device_interface});
+                            steps.push_back(sync_and_assert);
+                        }
+                        steps.push_back(set_current_func(stack.back()));
+
+                        if (other.defined()) {
+                            steps.push_back(mutate(other));
+                        }
+                        return Block::make(start_profiler,
+                                           LetStmt::make(op->name, mutate(op->value),
+                                                         Block::make(steps)));
+                    } else {
+                        internal_error << "No assert found after buffer copy.\n";
+                    }
+                }
+            }
+        }
+
+        Stmt body = mutate(op->body);
+        Expr value = mutate(op->value);
+        if (body.same_as(op->body) && value.same_as(op->value)) {
+            return op;
+        }
+        return LetStmt::make(op->name, value, body);
+    }
 };
 
 }  // namespace
diff --git a/src/runtime/HalideRuntime.h b/src/runtime/HalideRuntime.h
index f50e498ce88e..eea4faf7b073 100644
--- a/src/runtime/HalideRuntime.h
+++ b/src/runtime/HalideRuntime.h
@@ -885,6 +885,15 @@ extern int halide_device_release_crop(void *user_context,
  * should rarely be necessary, except maybe for profiling. */
 extern int halide_device_sync(void *user_context, struct halide_buffer_t *buf);
 
+/**
+ * Wait for current GPU operations to complete. Calling this explicitly
+ * should rarely be necessary, except maybe for profiling.
+ * This variation of the synchronizing is useful when a synchronization is desirable
+ * without specifying any buffer to synchronize on.
+ * Calling this with a null device_interface is always illegal.
+ */
+extern int halide_device_sync_global(void *user_context, const struct halide_device_interface_t *device_interface);
+
 /** Allocate device memory to back a halide_buffer_t. */
 extern int halide_device_malloc(void *user_context, struct halide_buffer_t *buf,
                                 const struct halide_device_interface_t *device_interface);
diff --git a/src/runtime/d3d12compute.cpp b/src/runtime/d3d12compute.cpp
index 9d652423ff9a..adae690800cc 100644
--- a/src/runtime/d3d12compute.cpp
+++ b/src/runtime/d3d12compute.cpp
@@ -2786,8 +2786,12 @@ WEAK int halide_d3d12compute_device_sync(void *user_context, struct halide_buffe
         return d3d12_context.error();
     }
 
-    d3d12_buffer *dbuffer = peel_buffer(buffer);
-    d3d12compute_device_sync_internal(d3d12_context.device, dbuffer);
+    if (buffer != nullptr) {
+        d3d12_buffer *dbuffer = peel_buffer(buffer);
+        d3d12compute_device_sync_internal(d3d12_context.device, dbuffer);
+    } else {
+        d3d12compute_device_sync_internal(d3d12_context.device, nullptr);
+    }
 
     return halide_error_code_success;
 }
diff --git a/src/runtime/device_interface.cpp b/src/runtime/device_interface.cpp
index 692a28e5fa9f..710d1259678d 100644
--- a/src/runtime/device_interface.cpp
+++ b/src/runtime/device_interface.cpp
@@ -231,6 +231,21 @@ WEAK int halide_device_sync(void *user_context, struct halide_buffer_t *buf) {
     }
 }
 
+/**
+ * Wait for current GPU operations to complete. Calling this explicitly
+ * should rarely be necessary, except maybe for profiling.
+ * This variation of the synchronizing is useful when a synchronization is desirable
+ * without specifying any buffer to synchronize on.
+ */
+WEAK int halide_device_sync_global(void *user_context, const struct halide_device_interface_t *device_interface) {
+    if (device_interface == nullptr) {
+        return halide_error_code_no_device_interface;
+    }
+    // This function calls immediately the device_interface implementation to syncrhonize on
+    //  "no buffer" (i.e., nullptr buffer) to trigger a "global" device sync.
+    return device_interface->impl->device_sync(user_context, nullptr);
+}
+
 /** Allocate device memory to back a halide_buffer_t. */
 WEAK int halide_device_malloc(void *user_context, struct halide_buffer_t *buf,
                               const halide_device_interface_t *device_interface) {
diff --git a/src/runtime/profiler_common.cpp b/src/runtime/profiler_common.cpp
index aed1376b6087..ccbe0bf11ecb 100644
--- a/src/runtime/profiler_common.cpp
+++ b/src/runtime/profiler_common.cpp
@@ -349,6 +349,14 @@ WEAK void halide_profiler_report_unlocked(void *user_context, halide_profiler_st
             };
         }
     }
+    bool support_colors = false;
+    const char *term = getenv("TERM");
+    if (term) {
+        // Check if the terminal supports colors
+        if (strstr(term, "color") || strstr(term, "xterm")) {
+            support_colors = true;
+        }
+    }
 
     for (halide_profiler_pipeline_stats *p = s->pipelines; p;
          p = (halide_profiler_pipeline_stats *)(p->next)) {
@@ -385,14 +393,31 @@ WEAK void halide_profiler_report_unlocked(void *user_context, halide_profiler_st
         if (print_f_states) {
             int f_stats_count = 0;
             halide_profiler_func_stats **f_stats = (halide_profiler_func_stats **)__builtin_alloca(p->num_funcs * sizeof(halide_profiler_func_stats *));
+            const char *substr_copy_to_device = " (copy to device)";
+            const char *substr_copy_to_host = " (copy to host)";
+
+            int max_func_name_length = 23;  // length of the section header
+            int num_copy_to_device = 0;
+            int num_copy_to_host = 0;
 
-            int max_func_name_length = 0;
+            uint64_t total_func_time = 0;
+            uint64_t total_copy_to_device_time = 0;
+            uint64_t total_copy_to_host_time = 0;
             for (int i = 0; i < p->num_funcs; i++) {
                 halide_profiler_func_stats *fs = p->funcs + i;
                 int name_len = strlen(fs->name);
                 if (name_len > max_func_name_length) {
                     max_func_name_length = name_len;
                 }
+                if (strstr(fs->name, substr_copy_to_device)) {
+                    num_copy_to_device++;
+                    total_copy_to_device_time += fs->time;
+                } else if (strstr(fs->name, substr_copy_to_host)) {
+                    num_copy_to_host++;
+                    total_copy_to_host_time += fs->time;
+                } else {
+                    total_func_time += fs->time;
+                }
             }
 
             for (int i = 0; i < p->num_funcs; i++) {
@@ -418,18 +443,8 @@ WEAK void halide_profiler_report_unlocked(void *user_context, halide_profiler_st
                 }
             }
 
-            for (int i = 0; i < f_stats_count; i++) {
-                size_t cursor = 0;
-                sstr.clear();
-                halide_profiler_func_stats *fs = f_stats[i];
-
-                sstr << "  " << fs->name << ": ";
-                cursor += max_func_name_length + 5;
-                while (sstr.size() < cursor) {
-                    sstr << " ";
-                }
-
-                float ft = fs->time / (p->runs * 1000000.0f);
+            const auto print_time_and_percentage = [&sstr, p](uint64_t time, size_t &cursor, bool light) {
+                float ft = time / (p->runs * 1000000.0f);
                 if (ft < 10000) {
                     sstr << " ";
                 }
@@ -451,16 +466,40 @@ WEAK void halide_profiler_report_unlocked(void *user_context, halide_profiler_st
                     sstr << " ";
                 }
 
-                int percent = 0;
+                int perthousand = 0;
                 if (p->time != 0) {
-                    percent = (100 * fs->time) / p->time;
+                    perthousand = (1000 * time) / p->time;
+                }
+                sstr << "(";
+                if (perthousand < 100) {
+                    sstr << " ";
                 }
-                sstr << "(" << percent << "%)";
-                cursor += 8;
+                int percent = perthousand / 10;
+                sstr << percent << "." << (perthousand - percent * 10) << "%)";
+                if (!light) {
+                    cursor += 10;
+                    while (sstr.size() < cursor) {
+                        sstr << " ";
+                    }
+                }
+            };
+
+            auto print_report_entry = [&](halide_profiler_func_stats *fs, const char *suffix_cut) {
+                size_t cursor = 0;
+                sstr.clear();
+
+                sstr << "    " << fs->name;
+                if (suffix_cut) {
+                    sstr.erase(strlen(suffix_cut));
+                }
+                sstr << ": ";
+                cursor += max_func_name_length + 7;
                 while (sstr.size() < cursor) {
                     sstr << " ";
                 }
 
+                print_time_and_percentage(fs->time, cursor, false);
+
                 if (!serial) {
                     float threads = fs->active_threads_numerator / (fs->active_threads_denominator + 1e-10);
                     sstr << "threads: " << threads;
@@ -494,6 +533,61 @@ WEAK void halide_profiler_report_unlocked(void *user_context, halide_profiler_st
                 sstr << "\n";
 
                 halide_print(user_context, sstr.str());
+            };
+
+            if (num_copy_to_host == 0 && num_copy_to_device == 0) {
+                for (int i = 0; i < f_stats_count; i++) {
+                    halide_profiler_func_stats *fs = f_stats[i];
+                    print_report_entry(fs, nullptr);
+                }
+            } else {
+                const auto print_section_header = [&](const char *name, uint64_t total_time) {
+                    size_t cursor = 0;
+                    sstr.clear();
+                    sstr << "  ";
+                    if (support_colors) {
+                        sstr << "\033[90m\033[3m";
+                        cursor += 9;
+                    }
+                    sstr << "[" << name << " ";
+                    cursor += max_func_name_length + 7;
+                    while (sstr.size() < cursor) {
+                        sstr << ":";
+                    }
+                    print_time_and_percentage(total_time, cursor, true);
+                    sstr << " ::::]";
+                    if (support_colors) {
+                        sstr << "\033[0m";
+                    }
+                    sstr << "\n";
+                    halide_print(user_context, sstr.str());
+                };
+
+                print_section_header("funcs", total_func_time);
+                for (int i = 0; i < f_stats_count; i++) {
+                    halide_profiler_func_stats *fs = f_stats[i];
+                    if (!strstr(fs->name, substr_copy_to_device) && !strstr(fs->name, substr_copy_to_host)) {
+                        print_report_entry(fs, nullptr);
+                    }
+                }
+                if (num_copy_to_device) {
+                    print_section_header("buffer copies to device", total_copy_to_device_time);
+                    for (int i = 0; i < f_stats_count; i++) {
+                        halide_profiler_func_stats *fs = f_stats[i];
+                        if (strstr(fs->name, substr_copy_to_device)) {
+                            print_report_entry(fs, substr_copy_to_device);
+                        }
+                    }
+                }
+                if (num_copy_to_host) {
+                    print_section_header("buffer copies to host", total_copy_to_host_time);
+                    for (int i = 0; i < f_stats_count; i++) {
+                        halide_profiler_func_stats *fs = f_stats[i];
+                        if (strstr(fs->name, substr_copy_to_host)) {
+                            print_report_entry(fs, substr_copy_to_host);
+                        }
+                    }
+                }
             }
         }
     }
diff --git a/src/runtime/runtime_api.cpp b/src/runtime/runtime_api.cpp
index 51f5b7245343..5c64391b6259 100644
--- a/src/runtime/runtime_api.cpp
+++ b/src/runtime/runtime_api.cpp
@@ -50,6 +50,7 @@ extern "C" __attribute__((used)) void *halide_runtime_api_functions[] = {
     (void *)&halide_device_malloc,
     (void *)&halide_device_release,
     (void *)&halide_device_sync,
+    (void *)&halide_device_sync_global,
     (void *)&halide_disable_timer_interrupt,
     (void *)&halide_do_par_for,
     (void *)&halide_do_parallel_tasks,
diff --git a/test/correctness/CMakeLists.txt b/test/correctness/CMakeLists.txt
index da968c419593..79de7462fd20 100644
--- a/test/correctness/CMakeLists.txt
+++ b/test/correctness/CMakeLists.txt
@@ -76,6 +76,7 @@ tests(GROUPS correctness
       debug_to_file_reorder.cpp
       deferred_loop_level.cpp
       deinterleave4.cpp
+      device_buffer_copies_with_profile.cpp
       device_buffer_copy.cpp
       device_copy_at_inner_loop.cpp
       device_crop.cpp
diff --git a/test/correctness/device_buffer_copies_with_profile.cpp b/test/correctness/device_buffer_copies_with_profile.cpp
new file mode 100644
index 000000000000..7398334fc06a
--- /dev/null
+++ b/test/correctness/device_buffer_copies_with_profile.cpp
@@ -0,0 +1,71 @@
+#include "Halide.h"
+
+using namespace Halide;
+
+int run_test(Target t) {
+    // Sliding window with the producer on the GPU and the consumer on
+    // the CPU. This requires a copy inside the loop over which we are
+    // sliding. Currently this copies the entire buffer back and
+    // forth, which is suboptimal in the general case. In this
+    // specific case we're folded over y, so copying the entire buffer
+    // is not much more than just copying the part that was modified.
+
+    Func f0{"f0_on_cpu"}, f1{"f1_on_gpu"}, f2{"f2_on_cpu"};
+    Var x, y, tx, ty;
+
+    // Produce something on CPU
+    f0(x, y) = x + y;
+    f0.compute_root();
+
+    // Which we use to produce something on GPU, causing a copy_to_device.
+    f1(x, y) = f0(x, y) + f0(x, y + 1);
+    f1.compute_root().gpu_tile(x, y, tx, ty, 8, 8);
+
+    // Which in turn we use to produce something on CPU, causing a copy_to_host.
+    f2(x, y) = f1(x, y) * 2;
+    f2.compute_root();
+
+    // Make the buffer a little bigger so we actually can see the copy time.
+    Buffer<int> out = f2.realize({2000, 2000}, t);
+    // Let's only verify a part of it...
+    for (int y = 0; y < 100; y++) {
+        for (int x = 0; x < 100; x++) {
+            int correct = 4 * (x + y) + 2;
+            if (out(x, y) != correct) {
+                printf("out(%d, %d) = %d instead of %d\n",
+                       x, y, out(x, y), correct);
+                return 1;
+            }
+        }
+    }
+    return 0;
+}
+
+int main(int argc, char **argv) {
+    Target t = get_jit_target_from_environment();
+    if (!t.has_gpu_feature()) {
+        printf("[SKIP] no gpu feature enabled\n");
+        return 0;
+    }
+    printf("Testing without profiler.\n");
+    int result = run_test(t);
+    if (result != 0) {
+        return 1;
+    }
+
+    printf("Testing thread based profiler.\n");
+    result = run_test(t.with_feature(Target::Profile));
+    if (result != 0) {
+        return 1;
+    }
+    if (t.os == Target::Linux) {
+        printf("Testing timer based profiler.\n");
+        result = run_test(t.with_feature(Target::ProfileByTimer));
+        if (result != 0) {
+            return 1;
+        }
+    }
+
+    printf("Success!\n");
+    return 0;
+}
diff --git a/test/performance/memory_profiler.cpp b/test/performance/memory_profiler.cpp
index 3fb511979265..8ca5cf3c2295 100644
--- a/test/performance/memory_profiler.cpp
+++ b/test/performance/memory_profiler.cpp
@@ -16,13 +16,13 @@ void reset_stats() {
 }
 
 void my_print(JITUserContext *, const char *msg) {
-    float this_ms, this_threads;
-    int idx, this_percentage, this_heap_peak;
+    float this_ms, this_threads, this_percentage;
+    int idx, this_heap_peak;
     int this_num_mallocs, this_malloc_avg, this_stack_peak;
     int val;
 
     // printf("%s", msg);
-    val = sscanf(msg, " g_%d: %fms (%d%%) threads: %f peak: %d num: %d avg: %d",
+    val = sscanf(msg, " g_%d: %fms (%f%%) threads: %f peak: %d num: %d avg: %d",
                  &idx, &this_ms, &this_percentage, &this_threads, &this_heap_peak,
                  &this_num_mallocs, &this_malloc_avg);
     if (val == 7) {
@@ -31,7 +31,7 @@ void my_print(JITUserContext *, const char *msg) {
         malloc_avg = this_malloc_avg;
     }
 
-    val = sscanf(msg, " g_%d: %fms (%d%%) peak: %d num: %d avg: %d",
+    val = sscanf(msg, " g_%d: %fms (%f%%) peak: %d num: %d avg: %d",
                  &idx, &this_ms, &this_percentage, &this_heap_peak,
                  &this_num_mallocs, &this_malloc_avg);
     if (val == 6) {
@@ -40,13 +40,13 @@ void my_print(JITUserContext *, const char *msg) {
         malloc_avg = this_malloc_avg;
     }
 
-    val = sscanf(msg, " g_%d: %fms (%d%%) threads: %f stack: %d",
+    val = sscanf(msg, " g_%d: %fms (%f%%) threads: %f stack: %d",
                  &idx, &this_ms, &this_percentage, &this_threads, &this_stack_peak);
     if (val == 5) {
         stack_peak = this_stack_peak;
     }
 
-    val = sscanf(msg, " g_%d: %fms (%d%%) stack: %d",
+    val = sscanf(msg, " g_%d: %fms (%f%%) stack: %d",
                  &idx, &this_ms, &this_percentage, &this_stack_peak);
     if (val == 4) {
         stack_peak = this_stack_peak;
diff --git a/test/performance/profiler.cpp b/test/performance/profiler.cpp
index 3912a16c4211..bf5d166c0e81 100644
--- a/test/performance/profiler.cpp
+++ b/test/performance/profiler.cpp
@@ -3,14 +3,14 @@
 
 using namespace Halide;
 
-int percentage = 0;
+float percentage = 0;
 float ms = 0;
 void my_print(JITUserContext *, const char *msg) {
     float this_ms;
-    int this_percentage;
-    int val = sscanf(msg, " fn13: %fms (%d", &this_ms, &this_percentage);
+    float this_percentage;
+    int val = sscanf(msg, " fn13: %fms (%f", &this_ms, &this_percentage);
     if (val != 2) {
-        val = sscanf(msg, " fn13$1: %fms (%d", &this_ms, &this_percentage);
+        val = sscanf(msg, " fn13$1: %fms (%f", &this_ms, &this_percentage);
     }
     if (val == 2) {
         ms = this_ms;
@@ -59,8 +59,8 @@ int run_test(bool use_timer_profiler) {
 
     printf("Time spent in fn13: %fms\n", ms);
 
-    if (percentage < 40) {
-        printf("Percentage of runtime spent in f13: %d\n"
+    if (percentage < 40.0f) {
+        printf("Percentage of runtime spent in f13: %.1f%%\n"
                "This is suspiciously low. It should be more like 66%%\n",
                percentage);
         return 1;
