Regarding point 2: When you feed a batch to your network, forward (including your dice roll) is called. When you calculate gradients by calling .backward on some scalar value (calculated using your network output), the gradient with respect to the weights that were actually used to compute the output (this depends the outcome of the dice roll) is computed. The gradient with respect to unused weights is not calculated. For example, if you roll coin == 2, for some weight w of self.middle_linear3 you should have w.grad == None.
EDIT: Note that as soon as self.middle_linear3 has previously been used at least once for a forward/backward call of a batch, w.grad will not be None anymore. If it is not used during a forward/backward call, it just won’t be updated/changed by calling .backward (usually w.grad will be zeros since one usually sets all gradients to zero between optimization steps).
Here is a code example hopefully explaining it well:
import torch
import torch.nn as nn
import random


class TestNet(nn.Module):
    def __init__(self):
        super(TestNet, self).__init__()
        self.fc_1 = nn.Linear(4, 1)
        self.fc_2 = nn.Linear(4, 1)

    def forward(self, x):
        if random.random() < 0.5:
            x = self.fc_1(x)
        else:
            x = self.fc_2(x)
        return x


net = TestNet()
data = torch.rand(32, 4)
out = net(data)
loss = sum(out)
loss.backward()

# We called forward/backward once, the gradients of the weights (parameters) of
# either fc_1 or fc_2 should be None
print("Weights and gradients after one f/b call")
for param in net.parameters():
    print(param)
    print("Gradient:", param.grad, "\n")

# Let's do 10 more forward/backward steps (setting grad to zero between steps)
for _ in range(10):
    out = net(data)
    loss = sum(out)
    net.zero_grad()
    loss.backward()

# Now (unless we were very unlucky (0.5**10-unlucky))
# all gradients are not None, and only the gradient with respect to the weights
# that were called in the last iteration are non-zero
print("Weights and gradients after 10 f/b calls")
for param in net.parameters():
    print(param)
    print("Gradient:", param.grad, "\n")


Partial answer to point 3: The recommended way is saving only the model weights. So I think this is how it works in your case: By saving the weights only it does not matter what your forward function looks like. Your DynamicNet object knows which modules (5x Linear) it contains of and saves their weights. You could create another Net using a completely different forward function as long as your modules are the same (and are named the same) and should be able to load your saved weights.