After consulting the PyTorch Docs a bit longer and seeing some other code examples, I post below my current forward function. Maybe it’s useful for some people; I actually haven’t found that many examples for this.
def forward(self, X_sorted, X_length_sorted, method='last_step'):
    X = self.word_embeddings(X_sorted)
    X = torch.transpose(X, 0, 1)
    X = nn.utils.rnn.pack_padded_sequence(X, X_length_sorted)
    X, self.hidden = self.gru(X, self.hidden)
    X, output_lengths = nn.utils.rnn.pad_packed_sequence(X)
    final_state = self.hidden.view(self.num_layers, self.directions_count, X_sorted.shape[0], self.rnn_hidden_dim)[-1]
    if self.directions_count == 1:
        X = final_state.squeeze()
    elif self.directions_count == 2:
        h_1, h_2 = final_state[0], final_state[1]  # forward & backward pass
        #X = h_1 + h_2                # Add both states
        X = torch.cat((h_1, h_2), 1)  # Concatenate both states
    # A series of fully connected layers
    for l in self.linears:
        X = l(X)
    return F.log_softmax(X, dim=1)

Of course, the size of the first linear layer depends whether I sum or concatenate the hidden states in case of a bidirectional RNN. Using my simple dataset at the moment, both approaches work equally well, but I don’t know if one approach is generally preferable.
To recap the original problem: When using PackedSequence, one cannot simple use the last output of the RNN (in my code X=X[-1]), since the dimension of X after pad_packed_sequence is the size of the longest sequence in the batch. For shorter ones, the RNN does go that far.