diff --git a/lib/dpif-offload-dummy.c b/lib/dpif-offload-dummy.c
index b7b24d6064..28f1f40013 100644
--- a/lib/dpif-offload-dummy.c
+++ b/lib/dpif-offload-dummy.c
@@ -42,8 +42,11 @@ struct pmd_id_data {
 struct dummy_offloaded_flow {
     struct hmap_node node;
     struct match match;
+    const struct nlattr *actions;
+    size_t actions_len;
     ovs_u128 ufid;
     uint32_t mark;
+    struct dpif_flow_stats stats;
 
     /* The pmd_id_map below is also protected by the port_mutex. */
     struct hmap pmd_id_map;
@@ -63,6 +66,19 @@ struct dummy_offload_port {
 
     struct ovs_mutex port_mutex; /* Protect all below members. */
     struct hmap offloaded_flows OVS_GUARDED;
+    struct ovs_list hw_recv_queue OVS_GUARDED;
+
+    /* Some simulated offload statistics. */
+    uint64_t rx_offload_partial OVS_GUARDED; /* Match found, CPU continues. */
+    uint64_t rx_offload_full OVS_GUARDED; /* Fully offloaded, CPU bypassed. */
+    uint64_t rx_offload_miss OVS_GUARDED; /* No HW offload rule matched. */
+    uint64_t rx_offload_pipe_abort OVS_GUARDED; /* Pipeline abort. */
+};
+
+struct hw_pkt_node {
+    struct dp_packet *pkt;
+    int queue_id;
+    struct ovs_list list_node;
 };
 
 static void dummy_flow_unreference(struct dummy_offload *, unsigned pmd_id,
@@ -228,6 +244,7 @@ dummy_free_flow(struct dummy_offload_port *port,
     ovs_assert(!hmap_count(&off_flow->pmd_id_map));
 
     hmap_destroy(&off_flow->pmd_id_map);
+    free(CONST_CAST(struct nlattr *, off_flow->actions));
     free(off_flow);
 }
 
@@ -288,6 +305,7 @@ dummy_free_port__(struct dummy_offload *offload,
                   struct dummy_offload_port *port, bool close_netdev)
 {
     struct dummy_offloaded_flow *off_flow;
+    struct hw_pkt_node *pkt;
 
     ovs_mutex_lock(&port->port_mutex);
     HMAP_FOR_EACH_POP (off_flow, node, &port->offloaded_flows) {
@@ -295,6 +313,12 @@ dummy_free_port__(struct dummy_offload *offload,
         dummy_free_flow(port, off_flow, false);
     }
     hmap_destroy(&port->offloaded_flows);
+
+    LIST_FOR_EACH_POP (pkt, list_node, &port->hw_recv_queue) {
+        dp_packet_delete(pkt->pkt);
+        free(pkt);
+    }
+
     ovs_mutex_unlock(&port->port_mutex);
     ovs_mutex_destroy(&port->port_mutex);
     if (close_netdev) {
@@ -330,11 +354,12 @@ dummy_offload_port_add(struct dpif_offload *dpif_offload,
                        struct netdev *netdev, odp_port_t port_no)
 {
     struct dummy_offload *offload = dummy_offload_cast(dpif_offload);
-    struct dummy_offload_port *port = xmalloc(sizeof *port);
+    struct dummy_offload_port *port = xzalloc(sizeof *port);
 
     ovs_mutex_init(&port->port_mutex);
     ovs_mutex_lock(&port->port_mutex);
     hmap_init(&port->offloaded_flows);
+    ovs_list_init(&port->hw_recv_queue);
     ovs_mutex_unlock(&port->port_mutex);
 
     if (dpif_offload_port_mgr_add(dpif_offload, &port->pm_port, netdev,
@@ -445,15 +470,27 @@ dummy_offload_get_debug(const struct dpif_offload *offload, struct ds *ds,
 {
     if (json) {
         struct json *json_ports = json_object_create();
-        struct dpif_offload_port *port;
+        struct dpif_offload_port *port_;
 
-        DPIF_OFFLOAD_PORT_FOR_EACH (port, offload) {
+        DPIF_OFFLOAD_PORT_FOR_EACH (port_, offload) {
+            struct dummy_offload_port *port = dummy_offload_port_cast(port_);
             struct json *json_port = json_object_create();
 
             json_object_put(json_port, "port_no",
-                            json_integer_create(odp_to_u32(port->port_no)));
-
-            json_object_put(json_ports, netdev_get_name(port->netdev),
+                            json_integer_create(odp_to_u32(port_->port_no)));
+
+            ovs_mutex_lock(&port->port_mutex);
+            json_object_put(json_port, "rx_offload_partial",
+                            json_integer_create(port->rx_offload_partial));
+            json_object_put(json_port, "rx_offload_full",
+                            json_integer_create(port->rx_offload_full));
+            json_object_put(json_port, "rx_offload_miss",
+                            json_integer_create(port->rx_offload_miss));
+            json_object_put(json_port, "rx_offload_pipe_abort",
+                            json_integer_create(port->rx_offload_pipe_abort));
+            ovs_mutex_unlock(&port->port_mutex);
+
+            json_object_put(json_ports, netdev_get_name(port_->netdev),
                             json_port);
         }
 
@@ -463,11 +500,22 @@ dummy_offload_get_debug(const struct dpif_offload *offload, struct ds *ds,
             json_destroy(json_ports);
         }
     } else if (ds) {
-        struct dpif_offload_port *port;
-
-        DPIF_OFFLOAD_PORT_FOR_EACH (port, offload) {
-            ds_put_format(ds, "  - %s: port_no: %u\n",
-                          netdev_get_name(port->netdev), port->port_no);
+        struct dpif_offload_port *port_;
+
+        DPIF_OFFLOAD_PORT_FOR_EACH (port_, offload) {
+            struct dummy_offload_port *port = dummy_offload_port_cast(port_);
+
+            ovs_mutex_lock(&port->port_mutex);
+            ds_put_format(ds,
+                          "  - %s: port_no: %u\n"
+                          "    rx_offload_partial   : %" PRIu64 "\n"
+                          "    rx_offload_full      : %" PRIu64 "\n"
+                          "    rx_offload_miss      : %" PRIu64 "\n"
+                          "    rx_offload_pipe_abort: %" PRIu64 "\n",
+                          netdev_get_name(port_->netdev), port_->port_no,
+                          port->rx_offload_partial, port->rx_offload_full,
+                          port->rx_offload_miss, port->rx_offload_pipe_abort);
+            ovs_mutex_unlock(&port->port_mutex);
         }
     }
 }
@@ -515,6 +563,19 @@ dummy_offload_get_port_by_netdev(const struct dpif_offload *offload,
     return dummy_offload_port_cast(port);
 }
 
+static struct dummy_offload_port *
+dummy_offload_get_port_by_odp_port(const struct dpif_offload *offload_,
+                                   odp_port_t port_no)
+{
+    struct dpif_offload_port *port;
+
+    port = dpif_offload_port_mgr_find_by_odp_port(offload_, port_no);
+    if (!port) {
+        return NULL;
+    }
+    return dummy_offload_port_cast(port);
+}
+
 static int
 dummy_offload_hw_post_process(const struct dpif_offload *offload_,
                               struct netdev *netdev, unsigned pmd_id,
@@ -549,6 +610,86 @@ dummy_offload_hw_post_process(const struct dpif_offload *offload_,
     return 0;
 }
 
+static bool
+dummy_offload_are_all_actions_supported(const struct dpif_offload *offload_,
+                                        odp_port_t in_odp,
+                                        const struct nlattr *actions,
+                                        size_t actions_len)
+{
+    const struct nlattr *nla;
+    size_t left;
+
+    /* Can we fully offload this flow? For now, only output actions are
+     * supported, and only to dummy-pmd netdevs where the egress port differs
+     * from the ingress port.  The latter restriction ensures that the partial
+     * offload test cases pass.
+     *
+     * The reason for supporting only dummy-pmd netdevs as output targets is
+     * that they provide full protection when calling netdev_send() from any
+     * thread, via a netdev-level mutex. */
+    NL_ATTR_FOR_EACH (nla, left, actions, actions_len) {
+        if (nl_attr_type(nla) == OVS_ACTION_ATTR_OUTPUT) {
+            odp_port_t out_odp = nl_attr_get_odp_port(nla);
+            struct dummy_offload_port *out_port;
+
+            out_port = dummy_offload_get_port_by_odp_port(offload_, out_odp);
+            if (out_odp == in_odp || !out_port
+                || strcmp("dummy-pmd",
+                          netdev_get_type(out_port->pm_port.netdev))) {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+    return true;
+}
+
+static bool
+dummy_offload_hw_process_pkt(const struct dpif_offload *offload_,
+                             struct dummy_offloaded_flow *flow,
+                             struct dp_packet *pkt)
+{
+    uint32_t hash = dp_packet_get_rss_hash(pkt);
+    uint32_t pkt_size = dp_packet_size(pkt);
+    const struct nlattr *nla;
+    size_t left;
+
+    if (!flow->actions) {
+        return false;
+    }
+
+    NL_ATTR_FOR_EACH (nla, left, flow->actions, flow->actions_len) {
+        bool last_action = (left <= NLA_ALIGN(nla->nla_len));
+
+        if (nl_attr_type(nla) == OVS_ACTION_ATTR_OUTPUT) {
+            odp_port_t odp_port = nl_attr_get_odp_port(nla);
+            struct dummy_offload_port *port;
+            struct dp_packet_batch batch;
+            int n_txq;
+
+            port = dummy_offload_get_port_by_odp_port(offload_, odp_port);
+            if (!port) {
+                return false;
+            }
+
+            n_txq = netdev_n_txq(port->pm_port.netdev);
+            dp_packet_batch_init_packet(&batch, last_action
+                                                ? pkt
+                                                : dp_packet_clone(pkt));
+            /* As the tx-steering option is not exposed to hardware offload,
+             * for now we assume hash steering based on the number of queues
+             * configured for the dummy-netdev. */
+            netdev_send(port->pm_port.netdev, hash % n_txq, &batch, false);
+        }
+    }
+
+    flow->stats.n_bytes += pkt_size;
+    flow->stats.n_packets++;
+    flow->stats.used = time_msec();
+    return true;
+}
+
 static int
 dummy_flow_put(const struct dpif_offload *offload_, struct netdev *netdev,
                struct dpif_offload_flow_put *put,
@@ -558,6 +699,7 @@ dummy_flow_put(const struct dpif_offload *offload_, struct netdev *netdev,
     struct dummy_offloaded_flow *off_flow;
     struct dummy_offload_port *port;
     bool modify = true;
+    bool full_offload;
     int error = 0;
 
     port = dummy_offload_get_port_by_netdev(offload_, netdev);
@@ -566,6 +708,10 @@ dummy_flow_put(const struct dpif_offload *offload_, struct netdev *netdev,
         goto exit;
     }
 
+    full_offload = dummy_offload_are_all_actions_supported(
+                        offload_, put->match->flow.in_port.odp_port,
+                        put->actions, put->actions_len);
+
     ovs_mutex_lock(&port->port_mutex);
 
     off_flow = dummy_find_offloaded_flow_and_update(
@@ -587,6 +733,14 @@ dummy_flow_put(const struct dpif_offload *offload_, struct netdev *netdev,
         *previous_flow_reference = NULL;
     }
     memcpy(&off_flow->match, put->match, sizeof *put->match);
+    free(CONST_CAST(struct nlattr *, off_flow->actions));
+    if (full_offload) {
+        off_flow->actions = xmemdup(put->actions, put->actions_len);
+        off_flow->actions_len = put->actions_len;
+    } else {
+        off_flow->actions = NULL;
+        off_flow->actions_len = 0;
+    }
 
     /* As we have per-netdev 'offloaded_flows', we don't need to match
      * the 'in_port' for received packets.  This will also allow offloading
@@ -609,13 +763,13 @@ dummy_flow_put(const struct dpif_offload *offload_, struct netdev *netdev,
     }
 
 exit_unlock:
-    ovs_mutex_unlock(&port->port_mutex);
-
-exit:
     if (put->stats) {
-        memset(put->stats, 0, sizeof *put->stats);
+        *put->stats = off_flow->stats;
     }
 
+    ovs_mutex_unlock(&port->port_mutex);
+
+exit:
     dummy_offload_log_operation(modify ? "modify" : "add", error, put->ufid);
     return error;
 }
@@ -650,6 +804,10 @@ dummy_flow_del(const struct dpif_offload *offload_, struct netdev *netdev,
         goto exit_unlock;
     }
 
+    if (del->stats) {
+        memcpy(del->stats, &off_flow->stats, sizeof *del->stats);
+    }
+
     mark = off_flow->mark;
     if (!hmap_count(&off_flow->pmd_id_map)) {
         dummy_free_flow_mark(offload, mark);
@@ -678,10 +836,6 @@ exit:
         ds_destroy(&ds);
     }
 
-    if (del->stats) {
-        memset(del->stats, 0, sizeof *del->stats);
-    }
-
     dummy_offload_log_operation("delete", error ? -1 : 0, del->ufid);
     return error ? ENOENT : 0;
 }
@@ -701,14 +855,19 @@ dummy_flow_stats(const struct dpif_offload *offload_, struct netdev *netdev,
 
     ovs_mutex_lock(&port->port_mutex);
     off_flow = dummy_find_offloaded_flow(port, ufid);
+    if (off_flow) {
+        memcpy(stats, &off_flow->stats, sizeof *stats);
+        attrs->dp_layer = off_flow->actions ? "dummy" : "ovs";
+        attrs->dp_extra_info = NULL;
+        attrs->offloaded = true;
+    }
     ovs_mutex_unlock(&port->port_mutex);
 
-    memset(stats, 0, sizeof *stats);
-    attrs->offloaded = off_flow ? true : false;
-    attrs->dp_layer = "ovs"; /* 'ovs', since this is a partial offload. */
-    attrs->dp_extra_info = NULL;
+    if (!off_flow) {
+        return false;
+    }
 
-    return off_flow ? true : false;
+    return true;
 }
 
 static void
@@ -729,23 +888,26 @@ dummy_flow_unreference(struct dummy_offload *offload, unsigned pmd_id,
     }
 }
 
-void
+bool
 dummy_netdev_simulate_offload(struct netdev *netdev, struct dp_packet *packet,
-                              struct flow *flow)
+                              int queue_id, struct flow *flow)
 {
     const struct dpif_offload *offload = ovsrcu_get(
         const struct dpif_offload *, &netdev->dpif_offload);
     struct dummy_offloaded_flow *data;
     struct dummy_offload_port *port;
+    bool packet_stolen = false;
     struct flow packet_flow;
+    bool offloaded = false;
 
-    if (!offload || strcmp(dpif_offload_type(offload), "dummy")) {
-        return;
+    if (!dpif_offload_enabled() || !offload
+        || strcmp(dpif_offload_type(offload), "dummy")) {
+        return false;
     }
 
     port = dummy_offload_get_port_by_netdev(offload, netdev);
     if (!port) {
-        return;
+        return false;
     }
 
     if (!flow) {
@@ -778,10 +940,84 @@ dummy_netdev_simulate_offload(struct netdev *netdev, struct dp_packet *packet,
                 VLOG_DBG("%s", ds_cstr(&ds));
                 ds_destroy(&ds);
             }
+
+            if (data->actions) {
+                /* Perform hardware offload simulation.  The packet is stolen
+                 * here and handed off to the PMD thread callback for
+                 * processing. */
+                struct hw_pkt_node *pkt_node = xmalloc(sizeof *pkt_node);
+
+                pkt_node->pkt = packet;
+                pkt_node->queue_id = queue_id;
+                ovs_list_push_back(&port->hw_recv_queue, &pkt_node->list_node);
+                packet_stolen = true;
+                port->rx_offload_full++;
+            } else {
+                port->rx_offload_partial++;
+            }
+
+            offloaded = true;
             break;
         }
     }
+
+    if (!offloaded) {
+        port->rx_offload_miss++;
+    }
+
     ovs_mutex_unlock(&port->port_mutex);
+    return packet_stolen;
+}
+
+void
+dummy_netdev_hw_offload_run(struct netdev *netdev)
+{
+    const struct dpif_offload *offload = ovsrcu_get(
+        const struct dpif_offload *, &netdev->dpif_offload);
+    struct dpif_offload_port *port_;
+
+    if (!dpif_offload_enabled() || !offload
+        || strcmp(dpif_offload_type(offload), "dummy")) {
+        return;
+    }
+
+    DPIF_OFFLOAD_PORT_FOR_EACH (port_, offload) {
+        struct dummy_offload_port *port;
+        struct hw_pkt_node *pkt_node;
+
+        port = dummy_offload_port_cast(port_);
+
+        if (ovs_mutex_trylock(&port->port_mutex)) {
+            continue;
+        }
+
+        LIST_FOR_EACH_POP (pkt_node, list_node, &port->hw_recv_queue) {
+            struct dummy_offloaded_flow *offloaded_flow;
+            struct dp_packet *pkt = pkt_node->pkt;
+            bool processed = false;
+            struct flow flow;
+
+            flow_extract(pkt, &flow);
+            HMAP_FOR_EACH (offloaded_flow, node, &port->offloaded_flows) {
+                if (flow_equal_except(&flow, &offloaded_flow->match.flow,
+                                      &offloaded_flow->match.wc)) {
+
+                    processed = dummy_offload_hw_process_pkt(
+                                    offload, offloaded_flow, pkt);
+                    break;
+                }
+            }
+
+            if (!processed) {
+                VLOG_DBG("Failed HW pipeline, sent to sw!");
+                port->rx_offload_pipe_abort++;
+                netdev_dummy_queue_simulate_offload_packet(
+                    port->pm_port.netdev, pkt, pkt_node->queue_id);
+            }
+            free(pkt_node);
+        }
+        ovs_mutex_unlock(&port->port_mutex);
+    }
 }
 
 #define DEFINE_DPIF_DUMMY_CLASS(NAME, TYPE_STR)                             \
diff --git a/lib/dummy.h b/lib/dummy.h
index e5c9d4071c..f250ae5933 100644
--- a/lib/dummy.h
+++ b/lib/dummy.h
@@ -41,10 +41,14 @@ void dummy_enable(const char *arg);
 /* Implementation details. */
 void dpif_dummy_register(enum dummy_level);
 void netdev_dummy_register(enum dummy_level);
+void netdev_dummy_queue_simulate_offload_packet(const struct netdev *,
+                                                struct dp_packet *,
+                                                int queue_id);
 void timeval_dummy_register(void);
 void ofpact_dummy_enable(void);
 bool is_dummy_netdev_class(const struct netdev_class *);
-void dummy_netdev_simulate_offload(struct netdev *, struct dp_packet *,
-                                   struct flow *);
+bool dummy_netdev_simulate_offload(struct netdev *, struct dp_packet *,
+                                   int queue_id, struct flow *);
+void dummy_netdev_hw_offload_run(struct netdev *);
 
 #endif /* dummy.h */
diff --git a/lib/netdev-dummy.c b/lib/netdev-dummy.c
index 7d3a7b9682..d5e8599349 100644
--- a/lib/netdev-dummy.c
+++ b/lib/netdev-dummy.c
@@ -695,6 +695,7 @@ netdev_dummy_run(const struct netdev_class *netdev_class)
         ovs_mutex_lock(&dev->mutex);
         dummy_packet_conn_run(dev);
         ovs_mutex_unlock(&dev->mutex);
+        dummy_netdev_hw_offload_run(&dev->up);
     }
     ovs_mutex_unlock(&dummy_list_mutex);
 }
@@ -1875,7 +1876,7 @@ eth_from_flow_str(const char *s, size_t packet_size,
 }
 
 static void
-netdev_dummy_queue_packet__(struct netdev_rxq_dummy *rx, struct dp_packet *packet)
+netdev_dummy_rxq_enqueue(struct netdev_rxq_dummy *rx, struct dp_packet *packet)
 {
     struct pkt_list_node *pkt_node = xmalloc(sizeof *pkt_node);
 
@@ -1886,35 +1887,64 @@ netdev_dummy_queue_packet__(struct netdev_rxq_dummy *rx, struct dp_packet *packe
 }
 
 static void
-netdev_dummy_queue_packet(struct netdev_dummy *dummy, struct dp_packet *packet,
-                          struct flow *flow, int queue_id)
+netdev_dummy_queue_packet__(struct netdev_dummy *dummy,
+                            struct dp_packet *packet, int queue_id)
     OVS_REQUIRES(dummy->mutex)
 {
-    struct netdev_rxq_dummy *rx, *prev;
-
-    if (dummy->rxq_pcap) {
-        ovs_pcap_write(dummy->rxq_pcap, packet);
-    }
+    struct netdev_rxq_dummy *rx, *prev = NULL;
 
-    dummy_netdev_simulate_offload(&dummy->up, packet, flow);
-
-    prev = NULL;
     LIST_FOR_EACH (rx, node, &dummy->rxes) {
         if (rx->up.queue_id == queue_id &&
             rx->recv_queue_len < NETDEV_DUMMY_MAX_QUEUE) {
             if (prev) {
-                netdev_dummy_queue_packet__(prev, dp_packet_clone(packet));
+                netdev_dummy_rxq_enqueue(prev, dp_packet_clone(packet));
             }
             prev = rx;
         }
     }
     if (prev) {
-        netdev_dummy_queue_packet__(prev, packet);
+        netdev_dummy_rxq_enqueue(prev, packet);
     } else {
         dp_packet_delete(packet);
     }
 }
 
+static void
+netdev_dummy_queue_packet(struct netdev_dummy *dummy, struct dp_packet *packet,
+                          struct flow *flow, int queue_id)
+    OVS_REQUIRES(dummy->mutex)
+{
+    if (dummy->rxq_pcap) {
+        ovs_pcap_write(dummy->rxq_pcap, packet);
+    }
+
+    if (dummy_netdev_simulate_offload(&dummy->up, packet,
+                                                   queue_id, flow)) {
+        /* Packet was stolen for full HW offload simulation. */
+        return;
+    }
+
+    netdev_dummy_queue_packet__(dummy, packet, queue_id);
+}
+
+void netdev_dummy_queue_simulate_offload_packet(const struct netdev *netdev,
+                                                struct dp_packet *packet,
+                                                int queue_id)
+{
+    struct netdev_dummy *dummy;
+
+    if (!netdev || !is_dummy_netdev_class(netdev->netdev_class)) {
+        dp_packet_delete(packet);
+        return;
+    }
+
+    dummy = netdev_dummy_cast(netdev);
+
+    ovs_mutex_lock(&dummy->mutex);
+    netdev_dummy_queue_packet__(dummy, packet, queue_id);
+    ovs_mutex_unlock(&dummy->mutex);
+}
+
 static void
 netdev_dummy_receive(struct unixctl_conn *conn,
                      int argc, const char *argv[], void *aux OVS_UNUSED)
diff --git a/tests/dpif-netdev.at b/tests/dpif-netdev.at
index 2311979709..0480730cab 100644
--- a/tests/dpif-netdev.at
+++ b/tests/dpif-netdev.at
@@ -32,6 +32,20 @@ strip_xout_keep_actions () {
 ' | sort
 }
 
+strip_hw_offload () {
+    sed '
+    /^flow-dump from/d
+    s/ufid:[-0-9a-f]*,//
+    s/used:[0-9\.][0-9\.]*/used:0.0/
+    s/[^,]*(0\/0),//g
+    s/eth([^)]*)/eth()/
+    s/ipv4([^)]*)/ipv4()/
+    s/udp([^)]*)/udp()/
+    s/, dp-extra-info.*//
+    s/^[[:space:]]*//
+' | sort
+}
+
 filter_flow_install () {
     grep 'flow_add' | sed 's/.*flow_add: //' | sort | uniq
 }
@@ -637,6 +651,116 @@ arp,in_port=ANY,dl_vlan=11,dl_vlan_pcp=7,vlan_tci1=0x0000,dl_src=00:06:07:08:09:
 DPIF_NETDEV_FLOW_HW_OFFLOAD_OFFSETS_VID_ARP([dummy])
 DPIF_NETDEV_FLOW_HW_OFFLOAD_OFFSETS_VID_ARP([dummy-pmd])
 
+AT_SETUP([dpif-netdev - full hw offload - dummy-pmd])
+OVS_VSWITCHD_START(
+  [add-port br0 p1 -- \
+   add-port br0 p2 -- \
+   set interface p1 type=dummy-pmd ofport_request=1 options:ifindex=1100 -- \
+   set interface p2 type=dummy-pmd ofport_request=2 options:ifindex=1200 \
+     options:tx_pcap=p2.pcap -- \
+   set bridge br0 datapath-type=dummy other-config:datapath-id=1234 \
+     fail-mode=secure], [], [], [--dummy-numa="0,0,0,0,1,1,1,1"])
+AT_CHECK([ovs-appctl vlog/set dpif:file:dbg dpif_netdev:file:dbg \
+          dpif_offload_dummy:file:dbg])
+
+AT_CHECK([ovs-vsctl set Open_vSwitch . other_config:hw-offload=true])
+OVS_WAIT_UNTIL([grep "Flow HW offload is enabled" ovs-vswitchd.log])
+
+AT_CHECK([ovs-ofctl del-flows br0])
+AT_CHECK([ovs-ofctl add-flow br0 in_port=1,actions=p2])
+
+packet="\
+  eth_src=00:06:07:08:09:0a,eth_dst=00:01:02:03:04:05,\
+  udp,ip_src=127.0.0.1,ip_dst=127.0.0.1,nw_ttl=64,\
+  udp_src=54392,udp_dst=5201"
+packet_hex=$(ovs-ofctl compose-packet --bare "${packet}")
+
+AT_CHECK([ovs-appctl netdev-dummy/receive p1 $packet_hex], [0])
+
+OVS_WAIT_UNTIL([grep "miss upcall" ovs-vswitchd.log])
+AT_CHECK([grep -A 1 'miss upcall' ovs-vswitchd.log | tail -n 1], [0], [dnl
+recirc_id(0),dp_hash(0),skb_priority(0),in_port(1),skb_mark(0),ct_state(0),ct_zone(0),ct_mark(0),ct_label(0),packet_type(ns=0,id=0),eth(src=00:06:07:08:09:0a,dst=00:01:02:03:04:05),eth_type(0x0800),ipv4(src=127.0.0.1,dst=127.0.0.1,proto=17,tos=0,ttl=64,frag=no),udp(src=54392,dst=5201)
+])
+
+# Check that flow successfully offloaded.
+OVS_WAIT_UNTIL([grep "succeed to add netdev flow" ovs-vswitchd.log])
+AT_CHECK([filter_hw_flow_install < ovs-vswitchd.log | strip_xout], [0], [dnl
+p1: flow put[[create]]: flow match: recirc_id=0,eth,ip,in_port=1,vlan_tci=0x0000/0x1fff,nw_frag=no, mark: 1
+])
+
+# Check that datapath flow installed successfully.
+AT_CHECK([filter_flow_install < ovs-vswitchd.log | strip_xout], [0], [dnl
+recirc_id(0),in_port(1),packet_type(ns=0,id=0),eth_type(0x0800),ipv4(frag=no), actions: <del>
+])
+
+# Inject the same packet again.
+AT_CHECK([ovs-appctl netdev-dummy/receive p1 $packet_hex], [0])
+
+# Check for succesfull packet matching with installed offloaded flow.
+AT_CHECK([filter_hw_packet_netdev_dummy < ovs-vswitchd.log | strip_xout], [0], [dnl
+p1: packet: udp,vlan_tci=0x0000,dl_src=00:06:07:08:09:0a,dl_dst=00:01:02:03:04:05,nw_src=127.0.0.1,nw_dst=127.0.0.1,nw_tos=0,nw_ecn=0,nw_ttl=64,nw_frag=no,tp_src=54392,tp_dst=5201 matches with flow: recirc_id=0,eth,ip,vlan_tci=0x0000/0x1fff,nw_frag=no with mark: 1
+])
+
+# Dump the datapath flow to see that actions was executed for a packet.
+AT_CHECK([ovs-appctl dpctl/dump-flows -m | strip_hw_offload], [0], [dnl
+recirc_id(0),in_port(p1),packet_type(ns=0,id=0),eth(),eth_type(0x0800),ipv4(),udp(), packets:1, bytes:106, used:0.0s, offloaded:yes, dp:dummy, actions:p2
+])
+
+# Wait for datapath flow expiration.
+ovs-appctl time/stop
+ovs-appctl time/warp 15000
+ovs-appctl revalidator/wait
+
+# Check that flow successfully deleted from HW.
+OVS_WAIT_UNTIL([grep "succeed to delete netdev flow" ovs-vswitchd.log])
+AT_CHECK([filter_hw_flow_del < ovs-vswitchd.log | strip_xout], [0], [dnl
+p1: flow del: mark: 1
+])
+
+# Check if we do not hit partial hw offload.
+AT_CHECK([ovs-appctl dpif-netdev/pmd-perf-show \
+          | grep -q "PHWOL hits:                  0 "])
+
+# Verify two packets where received.
+AT_CHECK([[[ $(ovs-pcap p2.pcap | grep -c "$packet_hex") -eq 2 ]]])
+
+# Verify that we observe one miss, one packet processed by hardware,
+# and no offload pipeline aborts.
+AT_CHECK(
+  [ovs-appctl --format json dpif/offload/show \
+     | sed 's/.*"p1":{\([[^}]]*\)}.*/\1/; s/,/\n/g; s/"//g' \
+     | sed -n '/^rx_offload_/p' | sort], [0], [dnl
+rx_offload_full:1
+rx_offload_miss:1
+rx_offload_partial:0
+rx_offload_pipe_abort:0
+])
+
+# In this test, we remove an output port while the revalidator is disabled.
+# The hardware flow remains, but the port can no longer be found, causing
+# the offload simulation to fail egressing.  This simulates an offload
+# pipeline abort, so the packet should fall back to normal processing.
+AT_CHECK([ovs-appctl netdev-dummy/receive p1 $packet_hex], [0])
+AT_CHECK([ovs-appctl revalidator/pause])
+AT_CHECK([ovs-vsctl del-port br0 p2])
+AT_CHECK([ovs-appctl netdev-dummy/receive p1 $packet_hex], [0])
+AT_CHECK([ovs-appctl dpctl/dump-flows -m | strip_hw_offload], [0], [dnl
+recirc_id(0),in_port(p1),packet_type(ns=0,id=0),eth(),eth_type(0x0800),ipv4(),udp(), packets:1, bytes:106, used:0.0s, offloaded:yes, dp:dummy, actions:2
+])
+AT_CHECK([ovs-appctl revalidator/resume])
+AT_CHECK(
+  [ovs-appctl --format json dpif/offload/show \
+     | sed 's/.*"p1":{\([[^}]]*\)}.*/\1/; s/,/\n/g; s/"//g' \
+     | sed -n '/^rx_offload_/p' | sort], [0], [dnl
+rx_offload_full:2
+rx_offload_miss:2
+rx_offload_partial:0
+rx_offload_pipe_abort:1
+])
+
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
 AT_SETUP([dpif-netdev - check dpctl/add-flow in_port exact match])
 OVS_VSWITCHD_START(
   [add-port br0 p1 \
diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at
index 39e43d3768..04dcb9a8ee 100644
--- a/tests/ofproto-dpif.at
+++ b/tests/ofproto-dpif.at
@@ -10196,6 +10196,18 @@ Globally enabled: false
 Datapaths:
 dummy@ovs-dummy:
   dummy
+    rx_offload_partial   : 0
+    rx_offload_full      : 0
+    rx_offload_miss      : 0
+    rx_offload_pipe_abort: 0
+    rx_offload_partial   : 0
+    rx_offload_full      : 0
+    rx_offload_miss      : 0
+    rx_offload_pipe_abort: 0
+    rx_offload_partial   : 0
+    rx_offload_full      : 0
+    rx_offload_miss      : 0
+    rx_offload_pipe_abort: 0
   dummy_x
   - br0: port_no: 100
   - br1: port_no: 101
@@ -10212,11 +10224,23 @@ AT_CHECK([ovs-appctl --format json --pretty dpif/offload/show], [0], [dnl
       "dummy": {
         "ports": {
           "br0": {
-            "port_no": 100},
+            "port_no": 100,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0},
           "br1": {
-            "port_no": 101},
+            "port_no": 101,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0},
           "ovs-dummy": {
-            "port_no": 0}}},
+            "port_no": 0,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0}}},
       "dummy_x": {
         }}},
   "enabled": false}
@@ -10248,6 +10272,18 @@ Globally enabled: false
 Datapaths:
 dummy@ovs-dummy:
   dummy_x
+    rx_offload_partial   : 0
+    rx_offload_full      : 0
+    rx_offload_miss      : 0
+    rx_offload_pipe_abort: 0
+    rx_offload_partial   : 0
+    rx_offload_full      : 0
+    rx_offload_miss      : 0
+    rx_offload_pipe_abort: 0
+    rx_offload_partial   : 0
+    rx_offload_full      : 0
+    rx_offload_miss      : 0
+    rx_offload_pipe_abort: 0
   dummy
   - br0: port_no: 100
   - br1: port_no: 101
@@ -10266,11 +10302,23 @@ AT_CHECK([ovs-appctl --format json --pretty dpif/offload/show], [0], [dnl
       "dummy_x": {
         "ports": {
           "br0": {
-            "port_no": 100},
+            "port_no": 100,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0},
           "br1": {
-            "port_no": 101},
+            "port_no": 101,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0},
           "ovs-dummy": {
-            "port_no": 0}}}}},
+            "port_no": 0,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0}}}}},
   "enabled": false}
 ])
 
@@ -10302,17 +10350,37 @@ AT_CHECK([ovs-appctl --format json --pretty dpif/offload/show], [0], [dnl
       "dummy": {
         "ports": {
           "br0": {
-            "port_no": 100},
+            "port_no": 100,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0},
           "ovs-dummy": {
-            "port_no": 0},
+            "port_no": 0,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0},
           "p4": {
-            "port_no": 4}}},
+            "port_no": 4,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0}}},
       "dummy_x": {
         "ports": {
           "p1": {
-            "port_no": 1},
+            "port_no": 1,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0},
           "p3": {
-            "port_no": 3}}}}},
+            "port_no": 3,
+            "rx_offload_full": 0,
+            "rx_offload_miss": 0,
+            "rx_offload_partial": 0,
+            "rx_offload_pipe_abort": 0}}}}},
   "enabled": false}
 ])
 
