ONOS-7739 Support for P4Runtime multicast programming

Design doc: https://docs.google.com/document/d/13rkQlwr49M-uxQQEuxCMP7BFEPY2gtQ850Hn3gUfesU/edit#heading=h.lzdayib259sq

Change-Id: Ief00bec89fe5a9784b0ee13fdaafa3ae58ab654f
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java
index a6c612b..0569f99 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java
@@ -46,5 +46,15 @@
     /**
      * Register entry.
      */
-    REGISTER_CELL
+    REGISTER_CELL,
+
+    /**
+     * Packet Replication Engine (PRE) multicast group entry.
+     */
+    PRE_MULTICAST_GROUP_ENTRY,
+
+    /**
+     * Packet Replication Engine (PRE) clone session entry.
+     */
+    PRE_CLONE_SESSION_ENTRY
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntry.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntry.java
new file mode 100644
index 0000000..5f68e40
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntry.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Collection;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Representation of multicast group entry of a protocol-independent packet
+ * replication engine (PRE).
+ */
+@Beta
+public final class PiMulticastGroupEntry implements PiPreEntry {
+
+    private final long groupId;
+    private final Set<PiPreReplica> replicas;
+
+    private PiMulticastGroupEntry(long groupId, Set<PiPreReplica> replicas) {
+        this.groupId = groupId;
+        this.replicas = replicas;
+    }
+
+    /**
+     * Returns the identifier of this multicast group, unique in the scope of a
+     * PRE instance.
+     *
+     * @return group entry ID
+     */
+    public long groupId() {
+        return groupId;
+    }
+
+    /**
+     * Returns the packet replicas provided by this multicast group.
+     *
+     * @return packet replicas
+     */
+    public Set<PiPreReplica> replicas() {
+        return replicas;
+    }
+
+    @Override
+    public PiPreEntryType preEntryType() {
+        return PiPreEntryType.MULTICAST_GROUP;
+    }
+
+    @Override
+    public PiEntityType piEntityType() {
+        return PiEntityType.PRE_MULTICAST_GROUP_ENTRY;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(groupId, replicas);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final PiMulticastGroupEntry other = (PiMulticastGroupEntry) obj;
+        return Objects.equal(this.groupId, other.groupId)
+                && Objects.equal(this.replicas, other.replicas);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("groupId", groupId)
+                .add("replicas", replicas)
+                .toString();
+    }
+
+    /**
+     * Returns a new builder of multicast group entries.
+     *
+     * @return builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder of PI multicast group entries.
+     */
+    public static final class Builder {
+
+        private Long groupId;
+        private ImmutableSet.Builder<PiPreReplica> replicaSetBuilder = ImmutableSet.builder();
+
+        private Builder() {
+            // Hide constructor.
+        }
+
+        /**
+         * Sets the identifier of this multicast group.
+         *
+         * @param groupId group ID
+         * @return this
+         */
+        public Builder withGroupId(long groupId) {
+            this.groupId = groupId;
+            return this;
+        }
+
+        /**
+         * Adds the given packet replica to this multicast group.
+         *
+         * @param replica packet replica
+         * @return this
+         */
+        public Builder addReplica(PiPreReplica replica) {
+            checkNotNull(replica);
+            replicaSetBuilder.add(replica);
+            return this;
+        }
+
+        /**
+         * Adds the given packet replicas to this multicast group.
+         *
+         * @param replicas packet replicas
+         * @return this
+         */
+        public Builder addReplicas(Collection<PiPreReplica> replicas) {
+            checkNotNull(replicas);
+            replicaSetBuilder.addAll(replicas);
+            return this;
+        }
+
+        /**
+         * Returns a new multicast group entry.
+         *
+         * @return multicast group entry
+         */
+        public PiMulticastGroupEntry build() {
+            checkNotNull(groupId, "Multicast group ID must be set");
+            final ImmutableSet<PiPreReplica> replicas = replicaSetBuilder.build();
+            checkArgument(!replicas.isEmpty(), "At least one replica must be defined");
+            return new PiMulticastGroupEntry(groupId, replicas);
+        }
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntryHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntryHandle.java
new file mode 100644
index 0000000..f9b1170
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntryHandle.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import org.onosproject.net.DeviceId;
+
+/**
+ * Global identifier of a PI multicast group entry applied to the packet
+ * replication engine of a device, uniquely defined by a device ID, and group
+ * ID.
+ */
+@Beta
+public final class PiMulticastGroupEntryHandle extends PiHandle<PiMulticastGroupEntry> {
+
+    private PiMulticastGroupEntryHandle(DeviceId deviceId, PiMulticastGroupEntry entry) {
+        super(deviceId, entry);
+    }
+
+    /**
+     * Creates a new handle for the given device ID and PI multicast group
+     * entry.
+     *
+     * @param deviceId device ID
+     * @param entry    PI multicast group entry
+     * @return PI multicast group entry handle
+     */
+    public static PiMulticastGroupEntryHandle of(DeviceId deviceId,
+                                                 PiMulticastGroupEntry entry) {
+        return new PiMulticastGroupEntryHandle(deviceId, entry);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(deviceId(), piEntity().groupId());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        PiMulticastGroupEntryHandle that = (PiMulticastGroupEntryHandle) o;
+        return Objects.equal(deviceId(), that.deviceId()) &&
+                Objects.equal(piEntity().groupId(), that.piEntity().groupId());
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("deviceId", deviceId())
+                .add("groupId", piEntity().groupId())
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPreEntry.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPreEntry.java
new file mode 100644
index 0000000..fb6c5e0
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPreEntry.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Configuration entry of a Packet Replication Engine (PRE) of
+ * protocol-independent pipeline.
+ */
+@Beta
+public interface PiPreEntry extends PiEntity {
+
+    /**
+     * Type of PRE entry.
+     */
+    enum PiPreEntryType {
+        /**
+         * Multicast group entry.
+         */
+        MULTICAST_GROUP,
+        /**
+         * Clone session entry.
+         */
+        CLONE_SESSION
+    }
+
+    /**
+     * Returns the type of this PRE entry.
+     *
+     * @return PRE entry type
+     */
+    PiPreEntryType preEntryType();
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPreReplica.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPreReplica.java
new file mode 100644
index 0000000..ea7003d
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiPreReplica.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.base.Objects;
+import org.onosproject.net.PortNumber;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+
+/**
+ * Representation of a packet replica used for multicast or cloning process in a
+ * protocol-independent packet replication engine.
+ * <p>
+ * Each replica is uniquely identified inside a given multicast group or clone
+ * session by the pair (egress port, instance ID).
+ */
+public class PiPreReplica {
+
+    private final PortNumber egressPort;
+    private final long instanceId;
+
+    /**
+     * Returns a new PRE packet replica for the given egress port and instance
+     * ID.
+     *
+     * @param egressPort egress port
+     * @param instanceId instance ID
+     */
+    public PiPreReplica(PortNumber egressPort, long instanceId) {
+        this.egressPort = checkNotNull(egressPort);
+        this.instanceId = instanceId;
+    }
+
+    /**
+     * Returns the egress port of this replica.
+     *
+     * @return egress port
+     */
+    public PortNumber egressPort() {
+        return egressPort;
+    }
+
+    /**
+     * Returns the instance ID of this replica.
+     *
+     * @return instance ID
+     */
+    public long instanceId() {
+        return instanceId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(egressPort, instanceId);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final PiPreReplica other = (PiPreReplica) obj;
+        return Objects.equal(this.egressPort, other.egressPort)
+                && Objects.equal(this.instanceId, other.instanceId);
+    }
+
+    @Override
+    public String toString() {
+        return format("%s:%d", egressPort, instanceId);
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiMulticastGroupTranslationStore.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiMulticastGroupTranslationStore.java
new file mode 100644
index 0000000..f961d84
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiMulticastGroupTranslationStore.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+
+/**
+ * A PI translation store that keeps track of which groups have been
+ * translated to which PI PRE multicast groups.
+ */
+@Beta
+public interface PiMulticastGroupTranslationStore
+        extends PiTranslationStore<Group, PiMulticastGroupEntry> {
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiMulticastGroupTranslator.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiMulticastGroupTranslator.java
new file mode 100644
index 0000000..e687189
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiMulticastGroupTranslator.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+
+/**
+ * A translator of groups to PI multicast group.
+ */
+@Beta
+public interface PiMulticastGroupTranslator
+        extends PiTranslator<Group, PiMulticastGroupEntry> {
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationService.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationService.java
index 20cef35..20ea14d 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationService.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationService.java
@@ -45,4 +45,12 @@
      * @return meter translator
      */
     PiMeterTranslator meterTranslator();
+
+    /**
+     * Returns a group translator for packet replication engine (PRE)
+     * multicast groups.
+     *
+     * @return multicast group translator
+     */
+    PiMulticastGroupTranslator multicastGroupTranslator();
 }
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntryTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntryTest.java
new file mode 100644
index 0000000..7c060ef
--- /dev/null
+++ b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiMulticastGroupEntryTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onosproject.net.PortNumber;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests for {@link PiMulticastGroupEntry}.
+ */
+public class PiMulticastGroupEntryTest {
+    private final long groupId1 = 1;
+    private final long groupId2 = 2;
+
+    private final long instanceId1 = 1;
+
+    private final PortNumber port1 = PortNumber.portNumber(1);
+    private final PortNumber port2 = PortNumber.portNumber(2);
+    private final PortNumber port3 = PortNumber.portNumber(3);
+
+    private final PiPreReplica replica1 = new PiPreReplica(port1, instanceId1);
+    private final PiPreReplica replica2 = new PiPreReplica(port2, instanceId1);
+    private final PiPreReplica replica3 = new PiPreReplica(port3, instanceId1);
+
+    private final PiMulticastGroupEntry group1 = PiMulticastGroupEntry.builder()
+            .withGroupId(groupId1)
+            .addReplica(replica1)
+            .addReplica(replica2)
+            .build();
+
+    private final PiMulticastGroupEntry sameAsGroup1 = PiMulticastGroupEntry.builder()
+            .withGroupId(groupId1)
+            .addReplica(replica1)
+            .addReplica(replica2)
+            .build();
+
+    private final PiMulticastGroupEntry group2 = PiMulticastGroupEntry.builder()
+            .withGroupId(groupId2)
+            .addReplica(replica1)
+            .addReplica(replica2)
+            .addReplica(replica3)
+            .build();
+
+    @Test
+    public void testPiMulticastGroupEntry() {
+        assertThat("Invalid group ID",
+                   group1.groupId(), is(groupId1));
+        assertThat("Invalid replicas size",
+                   group1.replicas().size(), is(2));
+        assertThat("Invalid replicas",
+                   group1.replicas(), contains(replica1, replica2));
+
+        assertThat("Invalid group ID",
+                   group2.groupId(), is(groupId2));
+        assertThat("Invalid replicas size",
+                   group2.replicas().size(), is(3));
+        assertThat("Invalid replicas",
+                   group2.replicas(), contains(replica1, replica2, replica3));
+    }
+
+    @Test
+    public void testEquality() {
+        new EqualsTester()
+                .addEqualityGroup(group1, sameAsGroup1)
+                .addEqualityGroup(group2)
+                .testEquals();
+    }
+}
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPreReplicaTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPreReplicaTest.java
new file mode 100644
index 0000000..85ea36e
--- /dev/null
+++ b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiPreReplicaTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onosproject.net.PortNumber;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests for {@link PiPreReplica}.
+ */
+public class PiPreReplicaTest {
+
+    private final long instanceId1 = 1;
+    private final long instanceId2 = 2;
+    private final PortNumber port1 = PortNumber.portNumber(1);
+    private final PortNumber port2 = PortNumber.portNumber(2);
+
+    private final PiPreReplica replica1of1 = new PiPreReplica(port1, instanceId1);
+    private final PiPreReplica sameAsReplica1of1 = new PiPreReplica(port1, instanceId1);
+
+    private final PiPreReplica replica1of2 = new PiPreReplica(port2, instanceId1);
+    private final PiPreReplica sameAsReplica1of2 = new PiPreReplica(port2, instanceId1);
+
+    private final PiPreReplica replica2of2 = new PiPreReplica(port2, instanceId2);
+    private final PiPreReplica sameAsReplica2of2 = new PiPreReplica(port2, instanceId2);
+
+    @Test
+    public void testPiPreReplica() {
+        assertThat("Invalid port", replica1of1.egressPort(), is(port1));
+        assertThat("Invalid instance ID", replica1of1.instanceId(), is(instanceId1));
+        assertThat("Invalid port", replica1of2.egressPort(), is(port2));
+        assertThat("Invalid instance ID", replica1of2.instanceId(), is(instanceId1));
+    }
+
+    @Test
+    public void testEquality() {
+        new EqualsTester()
+                .addEqualityGroup(replica1of1, sameAsReplica1of1)
+                .addEqualityGroup(replica1of2, sameAsReplica1of2)
+                .addEqualityGroup(replica2of2, sameAsReplica2of2)
+                .testEquals();
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImpl.java
new file mode 100644
index 0000000..3d524c7
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImpl.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.impl;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiPreReplica;
+import org.onosproject.net.pi.service.PiTranslationException;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+
+/**
+ * Implementation of multicast group translation logic.
+ */
+final class PiMulticastGroupTranslatorImpl {
+
+    private PiMulticastGroupTranslatorImpl() {
+        // Hides constructor.
+    }
+
+    /**
+     * Returns a PI PRE multicast group entry equivalent to the given group, for
+     * the given pipeconf and device.
+     * <p>
+     * The passed group is expected to have type {@link GroupDescription.Type#ALL}.
+     *
+     * @param group    group
+     * @return PI PRE entry
+     * @throws PiTranslationException if the group cannot be translated
+     */
+    static PiMulticastGroupEntry translate(Group group)
+            throws PiTranslationException {
+
+        checkNotNull(group);
+
+        if (!group.type().equals(GroupDescription.Type.ALL)) {
+            throw new PiTranslationException(format(
+                    "group type %s not supported", group.type()));
+        }
+
+        final List<Instruction> instructions = group.buckets().buckets().stream()
+                .flatMap(b -> b.treatment().allInstructions().stream())
+                .collect(Collectors.toList());
+
+        final boolean hasNonOutputInstr = instructions.stream()
+                .anyMatch(i -> !i.type().equals(Instruction.Type.OUTPUT));
+
+        if (instructions.size() != group.buckets().buckets().size()
+                || hasNonOutputInstr) {
+            throw new PiTranslationException(
+                    "support only groups with just one OUTPUT instruction per bucket");
+        }
+
+        final List<OutputInstruction> outInstructions = instructions.stream()
+                .map(i -> (OutputInstruction) i)
+                .collect(Collectors.toList());
+
+        return PiMulticastGroupEntry.builder()
+                .withGroupId(group.id().id())
+                .addReplicas(getReplicas(outInstructions))
+                .build();
+    }
+
+    private static Set<PiPreReplica> getReplicas(Collection<OutputInstruction> instructions) {
+        // Account for multiple replicas for the same port.
+        final Map<PortNumber, Set<PiPreReplica>> replicaMap = Maps.newHashMap();
+        final List<PortNumber> ports = instructions.stream()
+                .map(OutputInstruction::port)
+                .collect(Collectors.toList());
+        for (PortNumber port : ports) {
+            replicaMap.putIfAbsent(port, Sets.newHashSet());
+            // Use incremental instance IDs for replicas of the same port.
+            replicaMap.get(port).add(
+                    new PiPreReplica(port, replicaMap.get(port).size() + 1));
+        }
+        return replicaMap.values().stream()
+                .flatMap(Collection::stream)
+                .collect(Collectors.toSet());
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
index c69ced9..1ee53cf 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
@@ -31,6 +31,7 @@
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.runtime.PiActionGroup;
 import org.onosproject.net.pi.runtime.PiMeterCellConfig;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.service.PiFlowRuleTranslationStore;
 import org.onosproject.net.pi.service.PiFlowRuleTranslator;
@@ -38,6 +39,8 @@
 import org.onosproject.net.pi.service.PiGroupTranslator;
 import org.onosproject.net.pi.service.PiMeterTranslationStore;
 import org.onosproject.net.pi.service.PiMeterTranslator;
+import org.onosproject.net.pi.service.PiMulticastGroupTranslationStore;
+import org.onosproject.net.pi.service.PiMulticastGroupTranslator;
 import org.onosproject.net.pi.service.PiTranslationException;
 import org.onosproject.net.pi.service.PiTranslationService;
 import org.slf4j.Logger;
@@ -64,16 +67,21 @@
     private PiGroupTranslationStore groupTranslationStore;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private PiMulticastGroupTranslationStore mcGroupTranslationStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     private PiMeterTranslationStore meterTranslationStore;
 
     private PiFlowRuleTranslator flowRuleTranslator;
     private PiGroupTranslator groupTranslator;
+    private PiMulticastGroupTranslator mcGroupTranslator;
     private PiMeterTranslator meterTranslator;
 
     @Activate
     public void activate() {
         flowRuleTranslator = new InternalFlowRuleTranslator(flowRuleTranslationStore);
         groupTranslator = new InternalGroupTranslator(groupTranslationStore);
+        mcGroupTranslator = new InternalMulticastGroupTranslator(mcGroupTranslationStore);
         meterTranslator = new InternalMeterTranslator(meterTranslationStore);
         log.info("Started");
     }
@@ -101,6 +109,11 @@
         return meterTranslator;
     }
 
+    @Override
+    public PiMulticastGroupTranslator multicastGroupTranslator() {
+        return mcGroupTranslator;
+    }
+
     private Device getDevice(DeviceId deviceId) throws PiTranslationException {
         final Device device = deviceService.getDevice(deviceId);
         if (device == null) {
@@ -141,6 +154,21 @@
         }
     }
 
+    private final class InternalMulticastGroupTranslator
+            extends AbstractPiTranslatorImpl<Group, PiMulticastGroupEntry>
+            implements PiMulticastGroupTranslator {
+
+        private InternalMulticastGroupTranslator(PiMulticastGroupTranslationStore store) {
+            super(store);
+        }
+
+        @Override
+        public PiMulticastGroupEntry translate(Group original, PiPipeconf pipeconf)
+                throws PiTranslationException {
+            return PiMulticastGroupTranslatorImpl.translate(original);
+        }
+    }
+
     private final class InternalMeterTranslator
             extends AbstractPiTranslatorImpl<Meter, PiMeterCellConfig>
             implements PiMeterTranslator {
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImplTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImplTest.java
new file mode 100644
index 0000000..15774ac
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImplTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.impl;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.MacAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultFlowRule;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiMatchKey;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
+import org.onosproject.pipelines.basic.PipeconfLoader;
+
+import java.util.Optional;
+import java.util.Random;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_DST_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_SRC_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_TYPE_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.HDR_IN_PORT_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.TBL_TABLE0_ID;
+
+/**
+ * Test for {@link PiFlowRuleTranslatorImpl}.
+ */
+@SuppressWarnings("ConstantConditions")
+public class PiFlowRuleTranslatorImplTest {
+    private static final short IN_PORT_MASK = 0x01ff; // 9-bit mask
+    private static final short ETH_TYPE_MASK = (short) 0xffff;
+    private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:dummy:1");
+
+    private Random random = new Random();
+    private PiPipeconf pipeconf;
+
+    @Before
+    public void setUp() {
+        pipeconf = PipeconfLoader.BASIC_PIPECONF;
+    }
+
+    @Test
+    public void testTranslateFlowRules() throws Exception {
+
+        ApplicationId appId = new DefaultApplicationId(1, "test");
+        int tableId = 0;
+        MacAddress ethDstMac = MacAddress.valueOf(random.nextLong());
+        MacAddress ethSrcMac = MacAddress.valueOf(random.nextLong());
+        short ethType = (short) (0x0000FFFF & random.nextInt());
+        short outPort = (short) random.nextInt(65);
+        short inPort = (short) random.nextInt(65);
+        int timeout = random.nextInt(100);
+        int priority = random.nextInt(100);
+
+        TrafficSelector matchInPort1 = DefaultTrafficSelector
+                .builder()
+                .matchInPort(PortNumber.portNumber(inPort))
+                .matchEthDst(ethDstMac)
+                .matchEthSrc(ethSrcMac)
+                .matchEthType(ethType)
+                .build();
+
+        TrafficSelector emptySelector = DefaultTrafficSelector
+                .builder().build();
+
+        TrafficTreatment outPort2 = DefaultTrafficTreatment
+                .builder()
+                .setOutput(PortNumber.portNumber(outPort))
+                .build();
+
+        FlowRule rule1 = DefaultFlowRule.builder()
+                .forDevice(DEVICE_ID)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withSelector(matchInPort1)
+                .withTreatment(outPort2)
+                .makeTemporary(timeout)
+                .withPriority(priority)
+                .build();
+
+        FlowRule rule2 = DefaultFlowRule.builder()
+                .forDevice(DEVICE_ID)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withSelector(matchInPort1)
+                .withTreatment(outPort2)
+                .makeTemporary(timeout)
+                .withPriority(priority)
+                .build();
+
+        FlowRule defActionRule = DefaultFlowRule.builder()
+                .forDevice(DEVICE_ID)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withSelector(emptySelector)
+                .withTreatment(outPort2)
+                .makeTemporary(timeout)
+                .withPriority(priority)
+                .build();
+
+        PiTableEntry entry1 = PiFlowRuleTranslatorImpl.translate(rule1, pipeconf, null);
+        PiTableEntry entry2 = PiFlowRuleTranslatorImpl.translate(rule2, pipeconf, null);
+        PiTableEntry defActionEntry = PiFlowRuleTranslatorImpl.translate(defActionRule, pipeconf, null);
+
+        // check equality, i.e. same rules must produce same entries
+        new EqualsTester()
+                .addEqualityGroup(rule1, rule2)
+                .addEqualityGroup(entry1, entry2)
+                .testEquals();
+
+        // parse values stored in entry1
+        PiTernaryFieldMatch inPortParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_IN_PORT_ID).get();
+        PiTernaryFieldMatch ethDstParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_DST_ID).get();
+        PiTernaryFieldMatch ethSrcParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_SRC_ID).get();
+        PiTernaryFieldMatch ethTypeParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_TYPE_ID).get();
+        Optional<Double> expectedTimeout = pipeconf.pipelineModel().table(TBL_TABLE0_ID).get().supportsAging()
+                ? Optional.of((double) rule1.timeout()) : Optional.empty();
+
+        // check that values stored in entry are the same used for the flow rule
+        assertThat("Incorrect inPort match param value",
+                   inPortParam.value().asReadOnlyBuffer().getShort(), is(equalTo(inPort)));
+        assertThat("Incorrect inPort match param mask",
+                   inPortParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(IN_PORT_MASK)));
+        assertThat("Incorrect ethDestMac match param value",
+                   ethDstParam.value().asArray(), is(equalTo(ethDstMac.toBytes())));
+        assertThat("Incorrect ethDestMac match param mask",
+                   ethDstParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
+        assertThat("Incorrect ethSrcMac match param value",
+                   ethSrcParam.value().asArray(), is(equalTo(ethSrcMac.toBytes())));
+        assertThat("Incorrect ethSrcMac match param mask",
+                   ethSrcParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
+        assertThat("Incorrect ethType match param value",
+                   ethTypeParam.value().asReadOnlyBuffer().getShort(), is(equalTo(ethType)));
+        assertThat("Incorrect ethType match param mask",
+                   ethTypeParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(ETH_TYPE_MASK)));
+        // FIXME: re-enable when P4Runtime priority handling will be moved out of transltion service
+        // see PiFlowRuleTranslatorImpl
+        // assertThat("Incorrect priority value",
+        //            entry1.priority().get(), is(equalTo(MAX_PI_PRIORITY - rule1.priority())));
+        assertThat("Incorrect timeout value",
+                   entry1.timeout(), is(equalTo(expectedTimeout)));
+        assertThat("Match key should be empty",
+                   defActionEntry.matchKey(), is(equalTo(PiMatchKey.EMPTY)));
+        assertThat("Priority should not be set", !defActionEntry.priority().isPresent());
+    }
+}
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java
new file mode 100644
index 0000000..aeaa553
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiGroupTranslatorImplTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.impl;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.testing.EqualsTester;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.group.DefaultGroup;
+import org.onosproject.net.group.DefaultGroupBucket;
+import org.onosproject.net.group.DefaultGroupDescription;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionGroup;
+import org.onosproject.net.pi.runtime.PiActionGroupMember;
+import org.onosproject.net.pi.runtime.PiActionGroupMemberId;
+import org.onosproject.net.pi.runtime.PiActionParam;
+import org.onosproject.net.pi.runtime.PiGroupKey;
+import org.onosproject.net.pi.runtime.PiTableAction;
+import org.onosproject.pipelines.basic.PipeconfLoader;
+
+import java.util.Collection;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+import static org.onosproject.net.group.GroupDescription.Type.SELECT;
+import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRF_WCMP_SELECTOR_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRM_PORT_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.ACT_SET_EGRESS_PORT_WCMP_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.PORT_BITWIDTH;
+import static org.onosproject.pipelines.basic.BasicConstants.TBL_WCMP_TABLE_ID;
+
+/**
+ * Test for {@link PiGroupTranslatorImpl}.
+ */
+public class PiGroupTranslatorImplTest {
+
+    private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:dummy:1");
+    private static final ApplicationId APP_ID = TestApplicationId.create("dummy");
+    private static final GroupId GROUP_ID = GroupId.valueOf(1);
+    private static final PiGroupKey GROUP_KEY = new PiGroupKey(
+            TBL_WCMP_TABLE_ID, ACT_PRF_WCMP_SELECTOR_ID, GROUP_ID.id());
+    private static final List<GroupBucket> BUCKET_LIST = ImmutableList.of(
+            selectOutputBucket(1),
+            selectOutputBucket(2),
+            selectOutputBucket(3));
+    private static final GroupBuckets BUCKETS = new GroupBuckets(BUCKET_LIST);
+    private static final GroupDescription SELECT_GROUP_DESC = new DefaultGroupDescription(
+            DEVICE_ID, SELECT, BUCKETS, GROUP_KEY, GROUP_ID.id(), APP_ID);
+    private static final Group SELECT_GROUP = new DefaultGroup(GROUP_ID, SELECT_GROUP_DESC);
+    private static final int DEFAULT_MEMBER_WEIGHT = 1;
+    private static final int BASE_MEM_ID = 65535;
+    private Collection<PiActionGroupMember> expectedMembers;
+
+    private PiPipeconf pipeconf;
+
+    @Before
+    public void setUp() throws Exception {
+        pipeconf = PipeconfLoader.BASIC_PIPECONF;
+        expectedMembers = ImmutableSet.of(outputMember(1),
+                                          outputMember(2),
+                                          outputMember(3));
+    }
+
+    private static GroupBucket selectOutputBucket(int portNum) {
+        ImmutableByteSequence paramVal = copyFrom(portNum);
+        PiActionParam param = new PiActionParam(ACT_PRM_PORT_ID, paramVal);
+        PiTableAction action = PiAction.builder().withId(ACT_SET_EGRESS_PORT_WCMP_ID).withParameter(param).build();
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .add(Instructions.piTableAction(action))
+                .build();
+        return DefaultGroupBucket.createSelectGroupBucket(treatment);
+    }
+
+    private static PiActionGroupMember outputMember(int portNum)
+            throws ImmutableByteSequence.ByteSequenceTrimException {
+        PiActionParam param = new PiActionParam(ACT_PRM_PORT_ID, copyFrom(portNum).fit(PORT_BITWIDTH));
+        PiAction piAction = PiAction.builder()
+                .withId(ACT_SET_EGRESS_PORT_WCMP_ID)
+                .withParameter(param).build();
+        return PiActionGroupMember.builder()
+                .withAction(piAction)
+                .withId(PiActionGroupMemberId.of(BASE_MEM_ID + portNum))
+                .withWeight(DEFAULT_MEMBER_WEIGHT)
+                .build();
+    }
+
+    /**
+     * Test add group with buckets.
+     */
+    @Test
+    public void testTranslateGroups() throws Exception {
+
+        PiActionGroup piGroup1 = PiGroupTranslatorImpl.translate(SELECT_GROUP, pipeconf, null);
+        PiActionGroup piGroup2 = PiGroupTranslatorImpl.translate(SELECT_GROUP, pipeconf, null);
+
+        new EqualsTester()
+                .addEqualityGroup(piGroup1, piGroup2)
+                .testEquals();
+
+        assertThat("Group ID must be equal",
+                   piGroup1.id().id(), is(equalTo(GROUP_ID.id())));
+        assertThat("Action profile ID must be equal",
+                   piGroup1.actionProfileId(), is(equalTo(ACT_PRF_WCMP_SELECTOR_ID)));
+
+        // members installed
+        Collection<PiActionGroupMember> members = piGroup1.members();
+        assertThat("The number of group members must be equal",
+                   piGroup1.members().size(), is(expectedMembers.size()));
+        assertThat("Group members must be equal",
+                   members.containsAll(expectedMembers) && expectedMembers.containsAll(members));
+    }
+}
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImplTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImplTest.java
new file mode 100644
index 0000000..475b7d8
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiMulticastGroupTranslatorImplTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.net.pi.impl;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.group.DefaultGroup;
+import org.onosproject.net.group.DefaultGroupBucket;
+import org.onosproject.net.group.DefaultGroupDescription;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.pi.runtime.PiGroupKey;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiPreReplica;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.onosproject.net.group.GroupDescription.Type.ALL;
+import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRF_WCMP_SELECTOR_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.TBL_WCMP_TABLE_ID;
+
+/**
+ * Test for {@link PiMulticastGroupTranslatorImpl}.
+ */
+public class PiMulticastGroupTranslatorImplTest {
+    private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:dummy:1");
+    private static final ApplicationId APP_ID = TestApplicationId.create("dummy");
+    private static final GroupId GROUP_ID = GroupId.valueOf(1);
+    private static final PiGroupKey GROUP_KEY = new PiGroupKey(
+            TBL_WCMP_TABLE_ID, ACT_PRF_WCMP_SELECTOR_ID, GROUP_ID.id());
+
+    private static final List<GroupBucket> BUCKET_LIST = ImmutableList.of(
+            allOutputBucket(1),
+            allOutputBucket(2),
+            allOutputBucket(3));
+    private static final List<GroupBucket> BUCKET_LIST_2 = ImmutableList.of(
+            allOutputBucket(1),
+            allOutputBucket(2),
+            allOutputBucket(2),
+            allOutputBucket(3),
+            allOutputBucket(3));
+
+    private static final Set<PiPreReplica> REPLICAS = ImmutableSet.of(
+            new PiPreReplica(PortNumber.portNumber(1), 1),
+            new PiPreReplica(PortNumber.portNumber(2), 1),
+            new PiPreReplica(PortNumber.portNumber(3), 1));
+    private static final Set<PiPreReplica> REPLICAS_2 = ImmutableSet.of(
+            new PiPreReplica(PortNumber.portNumber(1), 1),
+            new PiPreReplica(PortNumber.portNumber(2), 1),
+            new PiPreReplica(PortNumber.portNumber(2), 2),
+            new PiPreReplica(PortNumber.portNumber(3), 1),
+            new PiPreReplica(PortNumber.portNumber(3), 2));
+
+    private static final PiMulticastGroupEntry PI_MULTICAST_GROUP =
+            PiMulticastGroupEntry.builder()
+                    .withGroupId(GROUP_ID.id())
+                    .addReplicas(REPLICAS)
+                    .build();
+    private static final PiMulticastGroupEntry PI_MULTICAST_GROUP_2 =
+            PiMulticastGroupEntry.builder()
+                    .withGroupId(GROUP_ID.id())
+                    .addReplicas(REPLICAS_2)
+                    .build();
+
+    private static final GroupBuckets BUCKETS = new GroupBuckets(BUCKET_LIST);
+    private static final GroupBuckets BUCKETS_2 = new GroupBuckets(BUCKET_LIST_2);
+
+    private static final GroupDescription ALL_GROUP_DESC = new DefaultGroupDescription(
+            DEVICE_ID, ALL, BUCKETS, GROUP_KEY, GROUP_ID.id(), APP_ID);
+    private static final Group ALL_GROUP = new DefaultGroup(GROUP_ID, ALL_GROUP_DESC);
+
+    private static final GroupDescription ALL_GROUP_DESC_2 = new DefaultGroupDescription(
+            DEVICE_ID, ALL, BUCKETS_2, GROUP_KEY, GROUP_ID.id(), APP_ID);
+    private static final Group ALL_GROUP_2 = new DefaultGroup(GROUP_ID, ALL_GROUP_DESC_2);
+
+
+    private static GroupBucket allOutputBucket(int portNum) {
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .setOutput(PortNumber.portNumber(portNum))
+                .build();
+        return DefaultGroupBucket.createAllGroupBucket(treatment);
+    }
+
+    @Test
+    public void testTranslatePreGroups() throws Exception {
+
+        PiMulticastGroupEntry multicastGroup = PiMulticastGroupTranslatorImpl
+                .translate(ALL_GROUP);
+        PiMulticastGroupEntry multicastGroup2 = PiMulticastGroupTranslatorImpl
+                .translate(ALL_GROUP_2);
+
+        new EqualsTester()
+                .addEqualityGroup(multicastGroup, PI_MULTICAST_GROUP)
+                .addEqualityGroup(multicastGroup2, PI_MULTICAST_GROUP_2)
+                .testEquals();
+    }
+}
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java
deleted file mode 100644
index 9faeaba..0000000
--- a/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * 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
- *
- * 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.
- */
-
-package org.onosproject.net.pi.impl;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.testing.EqualsTester;
-import org.junit.Before;
-import org.junit.Test;
-import org.onlab.packet.MacAddress;
-import org.onlab.util.ImmutableByteSequence;
-import org.onosproject.TestApplicationId;
-import org.onosproject.core.ApplicationId;
-import org.onosproject.core.DefaultApplicationId;
-import org.onosproject.core.GroupId;
-import org.onosproject.net.DeviceId;
-import org.onosproject.net.PortNumber;
-import org.onosproject.net.flow.DefaultFlowRule;
-import org.onosproject.net.flow.DefaultTrafficSelector;
-import org.onosproject.net.flow.DefaultTrafficTreatment;
-import org.onosproject.net.flow.FlowRule;
-import org.onosproject.net.flow.TrafficSelector;
-import org.onosproject.net.flow.TrafficTreatment;
-import org.onosproject.net.flow.instructions.Instructions;
-import org.onosproject.net.group.DefaultGroup;
-import org.onosproject.net.group.DefaultGroupBucket;
-import org.onosproject.net.group.DefaultGroupDescription;
-import org.onosproject.net.group.Group;
-import org.onosproject.net.group.GroupBucket;
-import org.onosproject.net.group.GroupBuckets;
-import org.onosproject.net.group.GroupDescription;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiAction;
-import org.onosproject.net.pi.runtime.PiActionGroup;
-import org.onosproject.net.pi.runtime.PiActionGroupMember;
-import org.onosproject.net.pi.runtime.PiActionGroupMemberId;
-import org.onosproject.net.pi.runtime.PiActionParam;
-import org.onosproject.net.pi.runtime.PiGroupKey;
-import org.onosproject.net.pi.runtime.PiMatchKey;
-import org.onosproject.net.pi.runtime.PiTableAction;
-import org.onosproject.net.pi.runtime.PiTableEntry;
-import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
-import org.onosproject.pipelines.basic.PipeconfLoader;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.Random;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.onlab.util.ImmutableByteSequence.copyFrom;
-import static org.onosproject.net.group.GroupDescription.Type.SELECT;
-import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRF_WCMP_SELECTOR_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRM_PORT_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.ACT_SET_EGRESS_PORT_WCMP_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_DST_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_SRC_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_TYPE_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.HDR_IN_PORT_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.PORT_BITWIDTH;
-import static org.onosproject.pipelines.basic.BasicConstants.TBL_TABLE0_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.TBL_WCMP_TABLE_ID;
-
-/**
- * Tests for {@link PiFlowRuleTranslatorImpl}.
- */
-@SuppressWarnings("ConstantConditions")
-public class PiTranslatorServiceTest {
-
-    private static final short IN_PORT_MASK = 0x01ff; // 9-bit mask
-    private static final short ETH_TYPE_MASK = (short) 0xffff;
-    private static final DeviceId DEVICE_ID = DeviceId.deviceId("device:dummy:1");
-    private static final ApplicationId APP_ID = TestApplicationId.create("dummy");
-    private static final GroupId GROUP_ID = GroupId.valueOf(1);
-    private static final List<GroupBucket> BUCKET_LIST = ImmutableList.of(outputBucket(1),
-                                                                          outputBucket(2),
-                                                                          outputBucket(3)
-    );
-    private static final PiGroupKey GROUP_KEY = new PiGroupKey(TBL_WCMP_TABLE_ID, ACT_PRF_WCMP_SELECTOR_ID,
-                                                               GROUP_ID.id());
-    private static final GroupBuckets BUCKETS = new GroupBuckets(BUCKET_LIST);
-    private static final GroupDescription GROUP_DESC =
-            new DefaultGroupDescription(DEVICE_ID, SELECT, BUCKETS, GROUP_KEY, GROUP_ID.id(), APP_ID);
-    private static final Group GROUP = new DefaultGroup(GROUP_ID, GROUP_DESC);
-    private static final int DEFAULT_MEMBER_WEIGHT = 1;
-    private static final int BASE_MEM_ID = 65535;
-    private Collection<PiActionGroupMember> expectedMembers;
-
-    private Random random = new Random();
-    private PiPipeconf pipeconf;
-
-    @Before
-    public void setUp() throws Exception {
-        pipeconf = PipeconfLoader.BASIC_PIPECONF;
-        expectedMembers = ImmutableSet.of(outputMember(1),
-                                          outputMember(2),
-                                          outputMember(3));
-    }
-
-    @Test
-    public void testTranslateFlowRules() throws Exception {
-
-        ApplicationId appId = new DefaultApplicationId(1, "test");
-        int tableId = 0;
-        MacAddress ethDstMac = MacAddress.valueOf(random.nextLong());
-        MacAddress ethSrcMac = MacAddress.valueOf(random.nextLong());
-        short ethType = (short) (0x0000FFFF & random.nextInt());
-        short outPort = (short) random.nextInt(65);
-        short inPort = (short) random.nextInt(65);
-        int timeout = random.nextInt(100);
-        int priority = random.nextInt(100);
-
-        TrafficSelector matchInPort1 = DefaultTrafficSelector
-                .builder()
-                .matchInPort(PortNumber.portNumber(inPort))
-                .matchEthDst(ethDstMac)
-                .matchEthSrc(ethSrcMac)
-                .matchEthType(ethType)
-                .build();
-
-        TrafficSelector emptySelector = DefaultTrafficSelector
-                .builder().build();
-
-        TrafficTreatment outPort2 = DefaultTrafficTreatment
-                .builder()
-                .setOutput(PortNumber.portNumber(outPort))
-                .build();
-
-        FlowRule rule1 = DefaultFlowRule.builder()
-                .forDevice(DEVICE_ID)
-                .forTable(tableId)
-                .fromApp(appId)
-                .withSelector(matchInPort1)
-                .withTreatment(outPort2)
-                .makeTemporary(timeout)
-                .withPriority(priority)
-                .build();
-
-        FlowRule rule2 = DefaultFlowRule.builder()
-                .forDevice(DEVICE_ID)
-                .forTable(tableId)
-                .fromApp(appId)
-                .withSelector(matchInPort1)
-                .withTreatment(outPort2)
-                .makeTemporary(timeout)
-                .withPriority(priority)
-                .build();
-
-        FlowRule defActionRule = DefaultFlowRule.builder()
-                .forDevice(DEVICE_ID)
-                .forTable(tableId)
-                .fromApp(appId)
-                .withSelector(emptySelector)
-                .withTreatment(outPort2)
-                .makeTemporary(timeout)
-                .withPriority(priority)
-                .build();
-
-        PiTableEntry entry1 = PiFlowRuleTranslatorImpl.translate(rule1, pipeconf, null);
-        PiTableEntry entry2 = PiFlowRuleTranslatorImpl.translate(rule2, pipeconf, null);
-        PiTableEntry defActionEntry = PiFlowRuleTranslatorImpl.translate(defActionRule, pipeconf, null);
-
-        // check equality, i.e. same rules must produce same entries
-        new EqualsTester()
-                .addEqualityGroup(rule1, rule2)
-                .addEqualityGroup(entry1, entry2)
-                .testEquals();
-
-        // parse values stored in entry1
-        PiTernaryFieldMatch inPortParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_IN_PORT_ID).get();
-        PiTernaryFieldMatch ethDstParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_DST_ID).get();
-        PiTernaryFieldMatch ethSrcParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_SRC_ID).get();
-        PiTernaryFieldMatch ethTypeParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(HDR_ETH_TYPE_ID).get();
-        Optional<Double> expectedTimeout = pipeconf.pipelineModel().table(TBL_TABLE0_ID).get().supportsAging()
-                ? Optional.of((double) rule1.timeout()) : Optional.empty();
-
-        // check that values stored in entry are the same used for the flow rule
-        assertThat("Incorrect inPort match param value",
-                   inPortParam.value().asReadOnlyBuffer().getShort(), is(equalTo(inPort)));
-        assertThat("Incorrect inPort match param mask",
-                   inPortParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(IN_PORT_MASK)));
-        assertThat("Incorrect ethDestMac match param value",
-                   ethDstParam.value().asArray(), is(equalTo(ethDstMac.toBytes())));
-        assertThat("Incorrect ethDestMac match param mask",
-                   ethDstParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
-        assertThat("Incorrect ethSrcMac match param value",
-                   ethSrcParam.value().asArray(), is(equalTo(ethSrcMac.toBytes())));
-        assertThat("Incorrect ethSrcMac match param mask",
-                   ethSrcParam.mask().asArray(), is(equalTo(MacAddress.BROADCAST.toBytes())));
-        assertThat("Incorrect ethType match param value",
-                   ethTypeParam.value().asReadOnlyBuffer().getShort(), is(equalTo(ethType)));
-        assertThat("Incorrect ethType match param mask",
-                   ethTypeParam.mask().asReadOnlyBuffer().getShort(), is(equalTo(ETH_TYPE_MASK)));
-        // FIXME: re-enable when P4Runtime priority handling will be moved out of transltion service
-        // see PiFlowRuleTranslatorImpl
-        // assertThat("Incorrect priority value",
-        //            entry1.priority().get(), is(equalTo(MAX_PI_PRIORITY - rule1.priority())));
-        assertThat("Incorrect timeout value",
-                   entry1.timeout(), is(equalTo(expectedTimeout)));
-        assertThat("Match key should be empty",
-                   defActionEntry.matchKey(), is(equalTo(PiMatchKey.EMPTY)));
-        assertThat("Priority should not be set", !defActionEntry.priority().isPresent());
-
-    }
-
-    private static GroupBucket outputBucket(int portNum) {
-        ImmutableByteSequence paramVal = copyFrom(portNum);
-        PiActionParam param = new PiActionParam(ACT_PRM_PORT_ID, paramVal);
-        PiTableAction action = PiAction.builder().withId(ACT_SET_EGRESS_PORT_WCMP_ID).withParameter(param).build();
-        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
-                .add(Instructions.piTableAction(action))
-                .build();
-        return DefaultGroupBucket.createSelectGroupBucket(treatment);
-    }
-
-    private static PiActionGroupMember outputMember(int portNum)
-            throws ImmutableByteSequence.ByteSequenceTrimException {
-        PiActionParam param = new PiActionParam(ACT_PRM_PORT_ID, copyFrom(portNum).fit(PORT_BITWIDTH));
-        PiAction piAction = PiAction.builder()
-                .withId(ACT_SET_EGRESS_PORT_WCMP_ID)
-                .withParameter(param).build();
-        return PiActionGroupMember.builder()
-                .withAction(piAction)
-                .withId(PiActionGroupMemberId.of(BASE_MEM_ID + portNum))
-                .withWeight(DEFAULT_MEMBER_WEIGHT)
-                .build();
-    }
-
-    /**
-     * Test add group with buckets.
-     */
-    @Test
-    public void testTranslateGroups() throws Exception {
-
-        PiActionGroup piGroup1 = PiGroupTranslatorImpl.translate(GROUP, pipeconf, null);
-        PiActionGroup piGroup2 = PiGroupTranslatorImpl.translate(GROUP, pipeconf, null);
-
-        new EqualsTester()
-                .addEqualityGroup(piGroup1, piGroup2)
-                .testEquals();
-
-        assertThat("Group ID must be equal",
-                   piGroup1.id().id(), is(equalTo(GROUP_ID.id())));
-        assertThat("Action profile ID must be equal",
-                   piGroup1.actionProfileId(), is(equalTo(ACT_PRF_WCMP_SELECTOR_ID)));
-
-        // members installed
-        Collection<PiActionGroupMember> members = piGroup1.members();
-        assertThat("The number of group members must be equal",
-                   piGroup1.members().size(), is(expectedMembers.size()));
-        assertThat("Group members must be equal",
-                   members.containsAll(expectedMembers) && expectedMembers.containsAll(members));
-    }
-}
diff --git a/core/store/dist/src/main/java/org/onosproject/store/pi/impl/DistributedPiMulticastGroupTranslationStore.java b/core/store/dist/src/main/java/org/onosproject/store/pi/impl/DistributedPiMulticastGroupTranslationStore.java
new file mode 100644
index 0000000..c95b185
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/pi/impl/DistributedPiMulticastGroupTranslationStore.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.store.pi.impl;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.service.PiMulticastGroupTranslationStore;
+
+/**
+ * Distributed implementation of a PI translation store for multicast groups.
+ */
+@Component(immediate = true)
+@Service
+public class DistributedPiMulticastGroupTranslationStore
+        extends AbstractDistributedPiTranslationStore<Group, PiMulticastGroupEntry>
+        implements PiMulticastGroupTranslationStore {
+
+    private static final String MAP_SIMPLE_NAME = "mc-group";
+
+    @Override
+    protected String mapSimpleName() {
+        return MAP_SIMPLE_NAME;
+    }
+}
diff --git a/drivers/bmv2/src/main/resources/bmv2-drivers.xml b/drivers/bmv2/src/main/resources/bmv2-drivers.xml
index 37b57e0..0575ee2 100644
--- a/drivers/bmv2/src/main/resources/bmv2-drivers.xml
+++ b/drivers/bmv2/src/main/resources/bmv2-drivers.xml
@@ -19,10 +19,12 @@
         <behaviour api="org.onosproject.net.behaviour.PiPipelineProgrammable"
                    impl="org.onosproject.drivers.bmv2.Bmv2PipelineProgrammable"/>
         <property name="tableDeleteBeforeUpdate">true</property>
+        <!--
         <behaviour api="org.onosproject.net.group.GroupProgrammable"
                    impl="org.onosproject.drivers.bmv2.Bmv2GroupProgrammable"/>
         <behaviour api="org.onosproject.net.device.DeviceHandshaker"
                    impl="org.onosproject.drivers.bmv2.Bmv2DeviceHandshaker"/>
+        -->
     </driver>
 </drivers>
 
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java
index 8676aee..15c1a0d 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java
@@ -16,13 +16,17 @@
 
 package org.onosproject.drivers.p4runtime;
 
-import com.google.common.collect.Maps;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Striped;
+import org.apache.commons.lang3.tuple.Pair;
 import org.onosproject.drivers.p4runtime.mirror.P4RuntimeGroupMirror;
+import org.onosproject.drivers.p4runtime.mirror.P4RuntimeMulticastGroupMirror;
 import org.onosproject.drivers.p4runtime.mirror.TimedEntry;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.group.DefaultGroup;
 import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupDescription;
 import org.onosproject.net.group.GroupOperation;
 import org.onosproject.net.group.GroupOperations;
 import org.onosproject.net.group.GroupProgrammable;
@@ -32,18 +36,20 @@
 import org.onosproject.net.pi.runtime.PiActionGroup;
 import org.onosproject.net.pi.runtime.PiActionGroupHandle;
 import org.onosproject.net.pi.runtime.PiActionGroupMember;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntryHandle;
 import org.onosproject.net.pi.service.PiGroupTranslator;
+import org.onosproject.net.pi.service.PiMulticastGroupTranslator;
 import org.onosproject.net.pi.service.PiTranslatedEntity;
 import org.onosproject.net.pi.service.PiTranslationException;
+import org.onosproject.p4runtime.api.P4RuntimeClient;
 import org.slf4j.Logger;
 
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -55,6 +61,10 @@
 
 /**
  * Implementation of the group programmable behaviour for P4Runtime.
+ * <p>
+ * This implementation distinguishes between ALL groups, and other types. ALL
+ * groups are handled via PRE multicast group programming, while other types are
+ * handled via action profile group programming.
  */
 public class P4RuntimeGroupProgrammable
         extends AbstractP4RuntimeHandlerBehaviour
@@ -80,11 +90,12 @@
 
     protected GroupStore groupStore;
     private P4RuntimeGroupMirror groupMirror;
-    private PiGroupTranslator translator;
+    private PiGroupTranslator groupTranslator;
+    private P4RuntimeMulticastGroupMirror mcGroupMirror;
+    private PiMulticastGroupTranslator mcGroupTranslator;
 
     // Needed to synchronize operations over the same group.
-    private static final Map<PiActionGroupHandle, Lock> GROUP_LOCKS =
-            Maps.newConcurrentMap();
+    private static final Striped<Lock> STRIPED_LOCKS = Striped.lock(30);
 
     @Override
     protected boolean setupBehaviour() {
@@ -92,8 +103,10 @@
             return false;
         }
         groupMirror = this.handler().get(P4RuntimeGroupMirror.class);
+        mcGroupMirror = this.handler().get(P4RuntimeMulticastGroupMirror.class);
         groupStore = handler().get(GroupStore.class);
-        translator = piTranslationService.groupTranslator();
+        groupTranslator = piTranslationService.groupTranslator();
+        mcGroupTranslator = piTranslationService.multicastGroupTranslator();
         return true;
     }
 
@@ -103,7 +116,17 @@
         if (!setupBehaviour()) {
             return;
         }
-        groupOps.operations().forEach(op -> processGroupOp(deviceId, op));
+        groupOps.operations().stream()
+                // Get group type and operation type
+                .map(op -> Pair.of(groupStore.getGroup(deviceId, op.groupId()),
+                                   op.opType()))
+                .forEach(pair -> {
+                    if (pair.getLeft().type().equals(GroupDescription.Type.ALL)) {
+                        processMcGroupOp(deviceId, pair.getLeft(), pair.getRight());
+                    } else {
+                        processGroupOp(deviceId, pair.getLeft(), pair.getRight());
+                    }
+                });
     }
 
     @Override
@@ -111,42 +134,75 @@
         if (!setupBehaviour()) {
             return Collections.emptyList();
         }
+        final ImmutableList.Builder<Group> groups = ImmutableList.builder();
+
         if (!driverBoolProperty(IGNORE_DEVICE_WHEN_GET, DEFAULT_IGNORE_DEVICE_WHEN_GET)) {
-            return pipeconf.pipelineModel().actionProfiles().stream()
-                    .map(PiActionProfileModel::id)
-                    .flatMap(this::streamGroupsFromDevice)
-                    .collect(Collectors.toList());
+            groups.addAll(pipeconf.pipelineModel().actionProfiles().stream()
+                                  .map(PiActionProfileModel::id)
+                                  .flatMap(this::streamGroupsFromDevice)
+                                  .iterator());
+            // FIXME: enable reading MC groups from device once reading from
+            // PRE is supported in PI
+            // groups.addAll(getMcGroupsFromDevice());
         } else {
-            return groupMirror.getAll(deviceId).stream()
-                    .map(TimedEntry::entry)
-                    .map(this::forgeGroupEntry)
-                    .collect(Collectors.toList());
+            groups.addAll(groupMirror.getAll(deviceId).stream()
+                                  .map(TimedEntry::entry)
+                                  .map(this::forgeGroupEntry)
+                                  .iterator());
         }
+        // FIXME: same as before..
+        groups.addAll(mcGroupMirror.getAll(deviceId).stream()
+                              .map(TimedEntry::entry)
+                              .map(this::forgeMcGroupEntry)
+                              .iterator());
+
+        return groups.build();
     }
 
-    private void processGroupOp(DeviceId deviceId, GroupOperation groupOp) {
-        final Group pdGroup = groupStore.getGroup(deviceId, groupOp.groupId());
-
+    private void processGroupOp(DeviceId deviceId, Group pdGroup, GroupOperation.Type opType) {
         final PiActionGroup piGroup;
         try {
-            piGroup = translator.translate(pdGroup, pipeconf);
+            piGroup = groupTranslator.translate(pdGroup, pipeconf);
         } catch (PiTranslationException e) {
-            log.warn("Unable translate group, aborting {} operation: {}",
-                     groupOp.opType(), e.getMessage());
+            log.warn("Unable to translate group, aborting {} operation: {} [{}]",
+                     opType, e.getMessage(), pdGroup);
             return;
         }
-
         final PiActionGroupHandle handle = PiActionGroupHandle.of(deviceId, piGroup);
 
         final PiActionGroup groupOnDevice = groupMirror.get(handle) == null
                 ? null
                 : groupMirror.get(handle).entry();
 
-        final Lock lock = GROUP_LOCKS.computeIfAbsent(handle, k -> new ReentrantLock());
+        final Lock lock = STRIPED_LOCKS.get(handle);
         lock.lock();
         try {
             processPiGroup(handle, piGroup,
-                           groupOnDevice, pdGroup, groupOp.opType());
+                           groupOnDevice, pdGroup, opType);
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    private void processMcGroupOp(DeviceId deviceId, Group pdGroup, GroupOperation.Type opType) {
+        final PiMulticastGroupEntry mcGroup;
+        try {
+            mcGroup = mcGroupTranslator.translate(pdGroup, pipeconf);
+        } catch (PiTranslationException e) {
+            log.warn("Unable to translate multicast group, aborting {} operation: {} [{}]",
+                     opType, e.getMessage(), pdGroup);
+            return;
+        }
+        final PiMulticastGroupEntryHandle handle = PiMulticastGroupEntryHandle.of(
+                deviceId, mcGroup);
+        final PiMulticastGroupEntry groupOnDevice = mcGroupMirror.get(handle) == null
+                ? null
+                : mcGroupMirror.get(handle).entry();
+        final Lock lock = STRIPED_LOCKS.get(handle);
+        lock.lock();
+        try {
+            processMcGroup(handle, mcGroup,
+                           groupOnDevice, pdGroup, opType);
         } finally {
             lock.unlock();
         }
@@ -167,7 +223,7 @@
 
             if (writeGroupToDevice(groupToApply)) {
                 groupMirror.put(handle, groupToApply);
-                translator.learn(handle, new PiTranslatedEntity<>(
+                groupTranslator.learn(handle, new PiTranslatedEntity<>(
                         pdGroup, groupToApply, handle));
             }
         } else if (operationType == GroupOperation.Type.MODIFY) {
@@ -183,8 +239,8 @@
             }
             if (modifyGroupFromDevice(groupToApply, groupOnDevice)) {
                 groupMirror.put(handle, groupToApply);
-                translator.learn(handle,
-                                 new PiTranslatedEntity<>(pdGroup, groupToApply, handle));
+                groupTranslator.learn(handle,
+                                      new PiTranslatedEntity<>(pdGroup, groupToApply, handle));
             }
         } else {
             if (groupOnDevice == null) {
@@ -194,11 +250,49 @@
             }
             if (deleteGroupFromDevice(groupOnDevice)) {
                 groupMirror.remove(handle);
-                translator.forget(handle);
+                groupTranslator.forget(handle);
             }
         }
     }
 
+    private void processMcGroup(PiMulticastGroupEntryHandle handle,
+                                PiMulticastGroupEntry groupToApply,
+                                PiMulticastGroupEntry groupOnDevice,
+                                Group pdGroup, GroupOperation.Type opType) {
+        if (opType == GroupOperation.Type.DELETE) {
+            if (writeMcGroupOnDevice(groupToApply, DELETE)) {
+                mcGroupMirror.remove(handle);
+                mcGroupTranslator.forget(handle);
+            }
+            return;
+        }
+
+        final P4RuntimeClient.WriteOperationType p4OpType =
+                opType == GroupOperation.Type.ADD ? INSERT : MODIFY;
+
+        if (driverBoolProperty(CHECK_MIRROR_BEFORE_UPDATE,
+                               DEFAULT_CHECK_MIRROR_BEFORE_UPDATE)
+                && p4OpType == MODIFY
+                && groupOnDevice != null
+                && groupOnDevice.equals(groupToApply)) {
+            // Ignore.
+            return;
+        }
+
+        if (writeMcGroupOnDevice(groupToApply, p4OpType)) {
+            mcGroupMirror.put(handle, groupToApply);
+            mcGroupTranslator.learn(handle, new PiTranslatedEntity<>(
+                    pdGroup, groupToApply, handle));
+        }
+    }
+
+    private boolean writeMcGroupOnDevice(PiMulticastGroupEntry group, P4RuntimeClient.WriteOperationType opType) {
+        return getFutureWithDeadline(
+                client.writePreMulticastGroupEntries(
+                        Collections.singleton(group), opType),
+                "performing multicast group " + opType, false);
+    }
+
     private boolean modifyGroupFromDevice(PiActionGroup groupToApply, PiActionGroup groupOnDevice) {
         PiActionProfileId groupProfileId = groupToApply.actionProfileId();
         Collection<PiActionGroupMember> membersToRemove = Sets.newHashSet(groupOnDevice.members());
@@ -287,9 +381,19 @@
                 .filter(Objects::nonNull);
     }
 
+    private Collection<Group> getMcGroupsFromDevice() {
+        Collection<PiMulticastGroupEntry> groups = getFutureWithDeadline(
+                client.readAllMulticastGroupEntries(),
+                "dumping multicast groups", Collections.emptyList());
+        return groups.stream()
+                .map(this::forgeMcGroupEntry)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
     private Group forgeGroupEntry(PiActionGroup piGroup) {
         final PiActionGroupHandle handle = PiActionGroupHandle.of(deviceId, piGroup);
-        if (!translator.lookup(handle).isPresent()) {
+        if (!groupTranslator.lookup(handle).isPresent()) {
             log.warn("Missing PI group from translation store: {} - {}:{}",
                      pipeconf.id(), piGroup.actionProfileId(),
                      piGroup.id());
@@ -297,7 +401,25 @@
         }
         final long life = groupMirror.get(handle) != null
                 ? groupMirror.get(handle).lifeSec() : 0;
-        final Group original = translator.lookup(handle).get().original();
+        final Group original = groupTranslator.lookup(handle).get().original();
+        return addedGroup(original, life);
+    }
+
+    private Group forgeMcGroupEntry(PiMulticastGroupEntry mcGroup) {
+        final PiMulticastGroupEntryHandle handle = PiMulticastGroupEntryHandle.of(
+                deviceId, mcGroup);
+        if (!mcGroupTranslator.lookup(handle).isPresent()) {
+            log.warn("Missing PI multicast group {} from translation store",
+                     mcGroup.groupId());
+            return null;
+        }
+        final long life = mcGroupMirror.get(handle) != null
+                ? mcGroupMirror.get(handle).lifeSec() : 0;
+        final Group original = mcGroupTranslator.lookup(handle).get().original();
+        return addedGroup(original, life);
+    }
+
+    private Group addedGroup(Group original, long life) {
         final DefaultGroup forgedGroup = new DefaultGroup(original.id(), original);
         forgedGroup.setState(Group.GroupState.ADDED);
         forgedGroup.setLife(life);
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMulticastGroupMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMulticastGroupMirror.java
new file mode 100644
index 0000000..4cfe0c5
--- /dev/null
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeMulticastGroupMirror.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.drivers.p4runtime.mirror;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntryHandle;
+import org.onosproject.store.serializers.KryoNamespaces;
+
+/**
+ * Distributed implementation of a P4Runtime multicast group mirror.
+ */
+@Component(immediate = true)
+@Service
+public final class DistributedP4RuntimeMulticastGroupMirror
+        extends AbstractDistributedP4RuntimeMirror
+                        <PiMulticastGroupEntryHandle, PiMulticastGroupEntry>
+        implements P4RuntimeMulticastGroupMirror {
+
+    private static final String DIST_MAP_NAME = "onos-p4runtime-mc-group-mirror";
+
+    @Override
+    String mapName() {
+        return DIST_MAP_NAME;
+    }
+
+    @Override
+    KryoNamespace storeSerializer() {
+        return KryoNamespace.newBuilder()
+                .register(KryoNamespaces.API)
+                .register(TimedEntry.class)
+                .build();
+    }
+}
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMulticastGroupMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMulticastGroupMirror.java
new file mode 100644
index 0000000..d901f29
--- /dev/null
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMulticastGroupMirror.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.drivers.p4runtime.mirror;
+
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntryHandle;
+
+/**
+ * Mirror of multicast groups installed on a P4Runtime device.
+ */
+public interface P4RuntimeMulticastGroupMirror
+        extends P4RuntimeMirror<PiMulticastGroupEntryHandle, PiMulticastGroupEntry> {
+}
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/FabricNextPipeliner.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/FabricNextPipeliner.java
index d8dcc8a..2ee66aa 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/FabricNextPipeliner.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/pipeliner/FabricNextPipeliner.java
@@ -17,7 +17,6 @@
 package org.onosproject.pipelines.fabric.pipeliner;
 
 import org.onlab.packet.VlanId;
-import org.onlab.util.ImmutableByteSequence;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.driver.Driver;
@@ -51,6 +50,7 @@
 import org.slf4j.Logger;
 
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 import static org.slf4j.LoggerFactory.getLogger;
@@ -160,12 +160,16 @@
         }
     }
 
-    private PortNumber getOutputPort(TrafficTreatment treatment) {
+    private Optional<OutputInstruction> getOutputInstruction(TrafficTreatment treatment) {
         return treatment.allInstructions()
                 .stream()
                 .filter(inst -> inst.type() == Instruction.Type.OUTPUT)
                 .map(inst -> (OutputInstruction) inst)
-                .findFirst()
+                .findFirst();
+    }
+
+    private PortNumber getOutputPort(TrafficTreatment treatment) {
+        return getOutputInstruction(treatment)
                 .map(OutputInstruction::port)
                 .orElse(null);
     }
@@ -302,60 +306,81 @@
     }
 
     private void processBroadcastNext(NextObjective next, PipelinerTranslationResult.Builder resultBuilder) {
-        int groupId = next.id();
-        List<GroupBucket> bucketList = next.next().stream()
-                .filter(treatment -> treatment != null)
-                .map(DefaultGroupBucket::createAllGroupBucket)
-                .collect(Collectors.toList());
-
-        if (bucketList.size() != next.next().size()) {
-            // some action not converted
-            // set error
-            log.warn("Expected bucket size {}, got {}", next.next().size(), bucketList.size());
+        final GroupDescription allGroup = getAllGroup(next);
+        if (allGroup == null) {
+            // Error already logged.
             resultBuilder.setError(ObjectiveError.BADPARAMS);
             return;
         }
 
-        GroupBuckets buckets = new GroupBuckets(bucketList);
-        //Used DefaultGroupKey instead of PiGroupKey
-        //as we don't have any action profile to apply to the groups of ALL type
-        GroupKey groupKey = new DefaultGroupKey(FabricPipeliner.KRYO.serialize(groupId));
-
-        resultBuilder.addGroup(new DefaultGroupDescription(deviceId,
-                                                           GroupDescription.Type.ALL,
-                                                           buckets,
-                                                           groupKey,
-                                                           groupId,
-                                                           next.appId()));
+        resultBuilder.addGroup(allGroup);
         //flow rule
-        TrafficSelector selector = buildNextIdSelector(next.id());
-        PiActionParam groupIdParam = new PiActionParam(FabricConstants.GID,
-                                                       ImmutableByteSequence.copyFrom(groupId));
+        final TrafficSelector selector = buildNextIdSelector(next.id());
+        final PiActionParam groupIdParam = new PiActionParam(
+                FabricConstants.GID, allGroup.givenGroupId());
 
-        PiAction setMcGroupAction = PiAction.builder()
+        final PiAction setMcGroupAction = PiAction.builder()
                 .withId(FabricConstants.FABRIC_INGRESS_NEXT_SET_MCAST_GROUP)
                 .withParameter(groupIdParam)
                 .build();
-        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+        final TrafficTreatment treatment = DefaultTrafficTreatment.builder()
                 .piTableAction(setMcGroupAction)
                 .build();
 
-        resultBuilder.addFlowRule(DefaultFlowRule.builder()
-                                          .withSelector(selector)
-                                          .withTreatment(treatment)
-                                          .forTable(FabricConstants.FABRIC_INGRESS_NEXT_MULTICAST)
-                                          .makePermanent()
-                                          .withPriority(next.priority())
-                                          .forDevice(deviceId)
-                                          .fromApp(next.appId())
-                                          .build());
+        resultBuilder.addFlowRule(
+                DefaultFlowRule.builder()
+                        .withSelector(selector)
+                        .withTreatment(treatment)
+                        .forTable(FabricConstants.FABRIC_INGRESS_NEXT_MULTICAST)
+                        .makePermanent()
+                        .withPriority(next.priority())
+                        .forDevice(deviceId)
+                        .fromApp(next.appId())
+                        .build());
 
         // Egress VLAN handling
-        next.next().forEach(trafficTreatment -> {
-            PortNumber outputPort = getOutputPort(trafficTreatment);
-            if (includesPopVlanInst(trafficTreatment) && outputPort != null) {
+        next.next().forEach(t -> {
+            PortNumber outputPort = getOutputPort(t);
+            if (includesPopVlanInst(t) && outputPort != null) {
                 processVlanPopRule(outputPort, next, resultBuilder);
             }
+            if (t.allInstructions().size() > 2) {
+                // More than OUTPUT and VLAN_POP...
+                log.warn("Some instructions of BROADCAST NextObjective might" +
+                                 "not have been applied, supported only " +
+                                 "OUTPUT and VLAN_POP, but found {}", t);
+            }
         });
     }
+
+    private GroupDescription getAllGroup(NextObjective next) {
+        final List<GroupBucket> bucketList = next.next().stream()
+                .map(this::getOutputInstruction)
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .map(i -> DefaultTrafficTreatment.builder().add(i).build())
+                .map(DefaultGroupBucket::createAllGroupBucket)
+                .collect(Collectors.toList());
+
+        if (bucketList.size() != next.next().size()) {
+            log.warn("Got BROADCAST NextObjective with {} treatments but " +
+                             "only {} have OUTPUT instructions, cannot " +
+                             "translate to ALL groups",
+                     next.next().size(), bucketList.size());
+            return null;
+        }
+
+        final int groupId = next.id();
+        final GroupBuckets buckets = new GroupBuckets(bucketList);
+        // Used DefaultGroupKey instead of PiGroupKey
+        // as we don't have any action profile to apply to the groups of ALL type
+        final GroupKey groupKey = new DefaultGroupKey(FabricPipeliner.KRYO.serialize(groupId));
+
+        return new DefaultGroupDescription(deviceId,
+                                           GroupDescription.Type.ALL,
+                                           buckets,
+                                           groupKey,
+                                           groupId,
+                                           next.appId());
+    }
 }
diff --git a/pipelines/fabric/src/test/java/org/onosproject/pipelines/fabric/pipeliner/FabricNextPipelinerTest.java b/pipelines/fabric/src/test/java/org/onosproject/pipelines/fabric/pipeliner/FabricNextPipelinerTest.java
index bfae578..bd57937 100644
--- a/pipelines/fabric/src/test/java/org/onosproject/pipelines/fabric/pipeliner/FabricNextPipelinerTest.java
+++ b/pipelines/fabric/src/test/java/org/onosproject/pipelines/fabric/pipeliner/FabricNextPipelinerTest.java
@@ -350,7 +350,13 @@
 
         //create the expected group
         GroupDescription actualGroup = groupsInstalled.get(0);
-        List<TrafficTreatment> treatments = ImmutableList.of(treatment1, treatment2);
+        TrafficTreatment groupTreatment1 = DefaultTrafficTreatment.builder()
+                .setOutput(PORT_1)
+                .build();
+        TrafficTreatment groupTreatment2 = DefaultTrafficTreatment.builder()
+                .setOutput(PORT_2)
+                .build();
+        List<TrafficTreatment> treatments = ImmutableList.of(groupTreatment1, groupTreatment2);
 
         List<GroupBucket> buckets = treatments.stream()
                 .map(DefaultGroupBucket::createAllGroupBucket)
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
index a9a6e08..93adf11 100644
--- a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
+++ b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeClient.java
@@ -28,6 +28,7 @@
 import org.onosproject.net.pi.runtime.PiCounterCellId;
 import org.onosproject.net.pi.runtime.PiMeterCellConfig;
 import org.onosproject.net.pi.runtime.PiMeterCellId;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 
@@ -225,4 +226,23 @@
      */
     CompletableFuture<Boolean> writeMeterCells(
             Collection<PiMeterCellConfig> cellConfigs, PiPipeconf pipeconf);
+
+    /**
+     * Performs the given write operation for the given PI multicast groups
+     * entries.
+     *
+     * @param entries multicast group entries
+     * @param opType  write operation type
+     * @return true if the operation was successful, false otherwise
+     */
+    CompletableFuture<Boolean> writePreMulticastGroupEntries(
+            Collection<PiMulticastGroupEntry> entries,
+            WriteOperationType opType);
+
+    /**
+     * Returns all multicast groups on device.
+     *
+     * @return multicast groups
+     */
+    CompletableFuture<Collection<PiMulticastGroupEntry>> readAllMulticastGroupEntries();
 }
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java
new file mode 100644
index 0000000..0be1ae8
--- /dev/null
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/MulticastGroupEntryCodec.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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
+ *
+ * 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.
+ */
+
+package org.onosproject.p4runtime.ctl;
+
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiPreReplica;
+import p4.v1.P4RuntimeOuterClass.MulticastGroupEntry;
+import p4.v1.P4RuntimeOuterClass.Replica;
+
+/**
+ * A coded of {@link PiMulticastGroupEntry} to P4Runtime MulticastGroupEntry
+ * messages, and vice versa.
+ */
+final class MulticastGroupEntryCodec {
+
+    private MulticastGroupEntryCodec() {
+        // Hides constructor.
+    }
+
+    /**
+     * Returns a P4Runtime MulticastGroupEntry message equivalent to the given
+     * PiMulticastGroupEntry.
+     *
+     * @param piEntry PiMulticastGroupEntry
+     * @return P4Runtime MulticastGroupEntry message
+     */
+    static MulticastGroupEntry encode(PiMulticastGroupEntry piEntry) {
+        final MulticastGroupEntry.Builder msgBuilder = MulticastGroupEntry.newBuilder();
+        msgBuilder.setMulticastGroupId(piEntry.groupId());
+        piEntry.replicas().stream()
+                .map(r -> Replica.newBuilder()
+                        .setEgressPort(r.egressPort().toLong())
+                        .setInstance(r.instanceId())
+                        .build())
+                .forEach(msgBuilder::addReplicas);
+        return msgBuilder.build();
+    }
+
+    /**
+     * Returns a PiMulticastGroupEntry equivalent to the given P4Runtime
+     * MulticastGroupEntry message.
+     *
+     * @param msg P4Runtime MulticastGroupEntry message
+     * @return PiMulticastGroupEntry
+     */
+    static PiMulticastGroupEntry decode(MulticastGroupEntry msg) {
+        final PiMulticastGroupEntry.Builder piEntryBuilder = PiMulticastGroupEntry.builder();
+        piEntryBuilder.withGroupId(msg.getMulticastGroupId());
+        msg.getReplicasList().stream()
+                .map(r -> new PiPreReplica(
+                        PortNumber.portNumber(r.getEgressPort()), r.getInstance()))
+                .forEach(piEntryBuilder::addReplica);
+        return piEntryBuilder.build();
+    }
+}
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
index a20a0e6..b4d88a7 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/P4RuntimeClientImpl.java
@@ -44,6 +44,7 @@
 import org.onosproject.net.pi.runtime.PiEntity;
 import org.onosproject.net.pi.runtime.PiMeterCellConfig;
 import org.onosproject.net.pi.runtime.PiMeterCellId;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.service.PiPipeconfService;
@@ -59,6 +60,8 @@
 import p4.v1.P4RuntimeOuterClass.Entity;
 import p4.v1.P4RuntimeOuterClass.ForwardingPipelineConfig;
 import p4.v1.P4RuntimeOuterClass.MasterArbitrationUpdate;
+import p4.v1.P4RuntimeOuterClass.MulticastGroupEntry;
+import p4.v1.P4RuntimeOuterClass.PacketReplicationEngineEntry;
 import p4.v1.P4RuntimeOuterClass.ReadRequest;
 import p4.v1.P4RuntimeOuterClass.ReadResponse;
 import p4.v1.P4RuntimeOuterClass.SetForwardingPipelineConfigRequest;
@@ -95,9 +98,11 @@
 import static org.slf4j.LoggerFactory.getLogger;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.ACTION_PROFILE_GROUP;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.ACTION_PROFILE_MEMBER;
+import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.PACKET_REPLICATION_ENGINE_ENTRY;
 import static p4.v1.P4RuntimeOuterClass.Entity.EntityCase.TABLE_ENTRY;
 import static p4.v1.P4RuntimeOuterClass.PacketIn;
 import static p4.v1.P4RuntimeOuterClass.PacketOut;
+import static p4.v1.P4RuntimeOuterClass.PacketReplicationEngineEntry.TypeCase.MULTICAST_GROUP_ENTRY;
 import static p4.v1.P4RuntimeOuterClass.SetForwardingPipelineConfigRequest.Action.VERIFY_AND_COMMIT;
 
 /**
@@ -155,8 +160,8 @@
     }
 
     /**
-     * Submits a task for async execution via the given executor.
-     * All tasks submitted with this method will be executed sequentially.
+     * Submits a task for async execution via the given executor. All tasks
+     * submitted with this method will be executed sequentially.
      */
     private <U> CompletableFuture<U> supplyWithExecutor(
             Supplier<U> supplier, String opDescription, Executor executor) {
@@ -292,6 +297,20 @@
     }
 
     @Override
+    public CompletableFuture<Boolean> writePreMulticastGroupEntries(
+            Collection<PiMulticastGroupEntry> entries,
+            WriteOperationType opType) {
+        return supplyInContext(() -> doWriteMulticastGroupEntries(entries, opType),
+                               "writePreMulticastGroupEntries");
+    }
+
+    @Override
+    public CompletableFuture<Collection<PiMulticastGroupEntry>> readAllMulticastGroupEntries() {
+        return supplyInContext(this::doReadAllMulticastGroupEntries,
+                               "readAllMulticastGroupEntries");
+    }
+
+    @Override
     public CompletableFuture<Collection<PiMeterCellConfig>> readMeterCells(Set<PiMeterCellId> cellIds,
                                                                            PiPipeconf pipeconf) {
         return supplyInContext(() -> doReadMeterCells(cellIds, pipeconf),
@@ -300,7 +319,7 @@
 
     @Override
     public CompletableFuture<Collection<PiMeterCellConfig>> readAllMeterCells(Set<PiMeterId> meterIds,
-                                                                                PiPipeconf pipeconf) {
+                                                                              PiPipeconf pipeconf) {
         return supplyInContext(() -> doReadAllMeterCells(meterIds, pipeconf),
                                "readAllMeterCells-" + meterIds.hashCode());
     }
@@ -319,7 +338,7 @@
 
     private boolean sendMasterArbitrationUpdate(Uint128 electionId) {
         log.info("Sending arbitration update to {}... electionId={}",
-                  deviceId, uint128ToBigInteger(electionId));
+                 deviceId, uint128ToBigInteger(electionId));
         try {
             streamRequestObserver.onNext(
                     StreamMessageRequest.newBuilder()
@@ -395,8 +414,6 @@
 
     private boolean doWriteTableEntries(Collection<PiTableEntry> piTableEntries, WriteOperationType opType,
                                         PiPipeconf pipeconf) {
-        WriteRequest.Builder writeRequestBuilder = WriteRequest.newBuilder();
-
         if (piTableEntries.size() == 0) {
             return true;
         }
@@ -419,19 +436,7 @@
             return false;
         }
 
-        writeRequestBuilder
-                .setDeviceId(p4DeviceId)
-                .setElectionId(clientElectionId)
-                .addAllUpdates(updateMsgs)
-                .build();
-
-        try {
-            blockingStub.write(writeRequestBuilder.build());
-            return true;
-        } catch (StatusRuntimeException e) {
-            checkAndLogWriteErrors(piTableEntries, e, opType, "table entry");
-            return false;
-        }
+        return write(updateMsgs, piTableEntries, opType, "table entry");
     }
 
     private Collection<PiTableEntry> doDumpTable(PiTableId piTableId, PiPipeconf pipeconf) {
@@ -444,6 +449,10 @@
             tableId = 0;
         } else {
             P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+            if (browser == null) {
+                log.warn("Unable to get a P4Info browser for pipeconf {}", pipeconf);
+                return Collections.emptyList();
+            }
             try {
                 tableId = browser.tables().getByName(piTableId.id()).getPreamble().getId();
             } catch (P4InfoBrowser.NotFoundException e) {
@@ -616,18 +625,7 @@
             return true;
         }
 
-        WriteRequest writeRequestMsg = WriteRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .setElectionId(clientElectionId)
-                .addAllUpdates(updateMsgs)
-                .build();
-        try {
-            blockingStub.write(writeRequestMsg);
-            return true;
-        } catch (StatusRuntimeException e) {
-            checkAndLogWriteErrors(members, e, opType, "group member");
-            return false;
-        }
+        return write(updateMsgs, members, opType, "group member");
     }
 
     private Collection<PiActionGroup> doDumpGroups(PiActionProfileId piActionProfileId, PiPipeconf pipeconf) {
@@ -762,23 +760,15 @@
             return false;
         }
 
-        final WriteRequest writeRequestMsg = WriteRequest.newBuilder()
-                .setDeviceId(p4DeviceId)
-                .setElectionId(clientElectionId)
-                .addUpdates(Update.newBuilder()
-                                    .setEntity(Entity.newBuilder()
-                                                       .setActionProfileGroup(actionProfileGroup)
-                                                       .build())
-                                    .setType(UPDATE_TYPES.get(opType))
-                                    .build())
+        final Update updateMsg = Update.newBuilder()
+                .setEntity(Entity.newBuilder()
+                                   .setActionProfileGroup(actionProfileGroup)
+                                   .build())
+                .setType(UPDATE_TYPES.get(opType))
                 .build();
-        try {
-            blockingStub.write(writeRequestMsg);
-            return true;
-        } catch (StatusRuntimeException e) {
-            checkAndLogWriteErrors(Collections.singleton(group), e, opType, "group");
-            return false;
-        }
+
+        return write(Collections.singleton(updateMsg), Collections.singleton(group),
+                     opType, "group");
     }
 
     private Collection<PiMeterCellConfig> doReadAllMeterCells(
@@ -830,11 +820,9 @@
         return MeterEntryCodec.decodeMeterEntities(responseEntities, pipeconf);
     }
 
-    private boolean doWriteMeterCells(Collection<PiMeterCellConfig> cellIds, PiPipeconf pipeconf) {
+    private boolean doWriteMeterCells(Collection<PiMeterCellConfig> cellConfigs, PiPipeconf pipeconf) {
 
-        WriteRequest.Builder writeRequestBuilder = WriteRequest.newBuilder();
-
-        Collection<Update> updateMsgs = MeterEntryCodec.encodePiMeterCellConfigs(cellIds, pipeconf)
+        Collection<Update> updateMsgs = MeterEntryCodec.encodePiMeterCellConfigs(cellConfigs, pipeconf)
                 .stream()
                 .map(meterEntryMsg ->
                              Update.newBuilder()
@@ -847,19 +835,91 @@
             return true;
         }
 
-        writeRequestBuilder
+        return write(updateMsgs, cellConfigs, WriteOperationType.MODIFY, "meter cell config");
+    }
+
+    private boolean doWriteMulticastGroupEntries(
+            Collection<PiMulticastGroupEntry> entries,
+            WriteOperationType opType) {
+
+        final List<Update> updateMsgs = entries.stream()
+                .map(MulticastGroupEntryCodec::encode)
+                .map(mcMsg -> PacketReplicationEngineEntry.newBuilder()
+                        .setMulticastGroupEntry(mcMsg)
+                        .build())
+                .map(preMsg -> Entity.newBuilder()
+                        .setPacketReplicationEngineEntry(preMsg)
+                        .build())
+                .map(entityMsg -> Update.newBuilder()
+                        .setEntity(entityMsg)
+                        .setType(UPDATE_TYPES.get(opType))
+                        .build())
+                .collect(Collectors.toList());
+        return write(updateMsgs, entries, opType, "multicast group entry");
+    }
+
+    private Collection<PiMulticastGroupEntry> doReadAllMulticastGroupEntries() {
+
+        final Entity entity = Entity.newBuilder()
+                .setPacketReplicationEngineEntry(
+                        PacketReplicationEngineEntry.newBuilder()
+                                .setMulticastGroupEntry(
+                                        MulticastGroupEntry.newBuilder()
+                                                .build())
+                                .build())
+                .build();
+
+        final ReadRequest req = ReadRequest.newBuilder()
+                .setDeviceId(p4DeviceId)
+                .addEntities(entity)
+                .build();
+
+        Iterator<ReadResponse> responses;
+        try {
+            responses = blockingStub.read(req);
+        } catch (StatusRuntimeException e) {
+            log.warn("Unable to read multicast group entries from {}: {}", deviceId, e.getMessage());
+            return Collections.emptyList();
+        }
+
+        Iterable<ReadResponse> responseIterable = () -> responses;
+        final List<PiMulticastGroupEntry> mcEntries = StreamSupport
+                .stream(responseIterable.spliterator(), false)
+                .map(ReadResponse::getEntitiesList)
+                .flatMap(List::stream)
+                .filter(e -> e.getEntityCase()
+                        .equals(PACKET_REPLICATION_ENGINE_ENTRY))
+                .map(Entity::getPacketReplicationEngineEntry)
+                .filter(e -> e.getTypeCase().equals(MULTICAST_GROUP_ENTRY))
+                .map(PacketReplicationEngineEntry::getMulticastGroupEntry)
+                .map(MulticastGroupEntryCodec::decode)
+                .collect(Collectors.toList());
+
+        log.debug("Retrieved {} multicast group entries from {}...",
+                  mcEntries.size(), deviceId);
+
+        return mcEntries;
+    }
+
+    private <E extends PiEntity> boolean write(Collection<Update> updates,
+                                               Collection<E> writeEntities,
+                                               WriteOperationType opType,
+                                               String entryType) {
+        try {
+            blockingStub.write(writeRequest(updates));
+            return true;
+        } catch (StatusRuntimeException e) {
+            checkAndLogWriteErrors(writeEntities, e, opType, entryType);
+            return false;
+        }
+    }
+
+    private WriteRequest writeRequest(Iterable<Update> updateMsgs) {
+        return WriteRequest.newBuilder()
                 .setDeviceId(p4DeviceId)
                 .setElectionId(clientElectionId)
                 .addAllUpdates(updateMsgs)
                 .build();
-        try {
-            blockingStub.write(writeRequestBuilder.build());
-            return true;
-        } catch (StatusRuntimeException e) {
-            log.warn("Unable to write meter entries : {}", e.getMessage());
-            log.debug("exception", e);
-            return false;
-        }
     }
 
     private Void doShutdown() {
@@ -926,7 +986,7 @@
             errors.stream()
                     .filter(err -> err.getCanonicalCode() != Status.OK.getCode().value())
                     .forEach(err -> log.warn("Unable to {} {} (unknown): {}",
-                                 opType.name(), entryType, parseP4Error(err)));
+                                             opType.name(), entryType, parseP4Error(err)));
         }
     }
 
diff --git a/tools/dev/p4vm/bm-commands.sh b/tools/dev/p4vm/bm-commands.sh
index fdb8340..599f5c0 100644
--- a/tools/dev/p4vm/bm-commands.sh
+++ b/tools/dev/p4vm/bm-commands.sh
@@ -7,7 +7,7 @@
     fi
     tport=$(head -n 1 /tmp/bmv2-$1-thrift-port)
     echo "Starting CLI for BMv2 instance $1 (Thrift port $tport)..."
-    sudo bm_CLI --thrift-port ${tport} ${@:2}
+    sudo bm_CLI --thrift-port ${tport} --pre SimplePreLAG ${@:2}
 }
 
 bm-dbg () {