I like to use indexing and broadcasting for this:
out = x[:, :, None] * emb[None, :, :]

Indexing with None inserts a new dimension (“unsqueeze” or “view” could achieve the same, but I like to use the above to help me see the alignment).
For more advanced uses, torch.einsum 7 can be useful, too. Here, it would be
torch.einsum('bn,nm->bnm', [x, emb]). Here the indices replicate your indexing (except that I use the dimension indices).
Best regards
Thomas