Directly support assert on Scalar, instead of forcing Tensor
Labels: triaged, oncall: pt2, module: export

### 🐛 Describe the bug

Repro:

```
import torch
import torch._dynamo.comptime
def f(x):
    y = x.item()
    torch.export.constrain_as_size(y)
    return torch.zeros(y)
print(torch.export.export(f, (torch.tensor([3]),)))
```

This prints

```
    class GraphModule(torch.nn.Module):
        def forward(self, arg0_1: i64[1]):
            # File: /data/users/ezyang/c/pytorch/wz.py:4, code: y = x.item()
            _local_scalar_dense: Sym(i4) = torch.ops.aten._local_scalar_dense.default(arg0_1);  arg0_1 = None
            
            # File: /data/users/ezyang/c/pytorch/torch/_export/pass_base.py:45, code: return NodeMetadata({"stack_trace": "".join(traceback.format_stack(limit=1))})
            ge: Sym(i4 >= 0) = _local_scalar_dense >= 0
            scalar_tensor: f32[] = torch.ops.aten.scalar_tensor.default(ge);  ge = None
            _assert_async = torch.ops.aten._assert_async.msg(scalar_tensor, '_local_scalar_dense is outside of inline constraint [0, inf].');  scalar_tensor = None
            
            # File: /data/users/ezyang/c/pytorch/wz.py:5, code: torch.export.constrain_as_size(y)
            sym_constrain_range_for_size = torch.ops.aten.sym_constrain_range_for_size.default(_local_scalar_dense, min = None, max = None)
            
            # File: /data/users/ezyang/c/pytorch/wz.py:6, code: return torch.zeros(y)
            full: f32[i4] = torch.ops.aten.full.default([_local_scalar_dense], 0, dtype = torch.float32, layout = torch.strided, device = device(type='cpu'), pin_memory = False);  _local_scalar_dense = None
            
            # File: /data/users/ezyang/c/pytorch/torch/_export/pass_base.py:45, code: return NodeMetadata({"stack_trace": "".join(traceback.format_stack(limit=1))})
            sym_size: Sym(i4) = torch.ops.aten.sym_size.int(full, 0)
            ge_1: Sym(i4 >= 0) = sym_size >= 0;  sym_size = None
            scalar_tensor_1: f32[] = torch.ops.aten.scalar_tensor.default(ge_1);  ge_1 = None
            _assert_async_1 = torch.ops.aten._assert_async.msg(scalar_tensor_1, 'full.shape[0] is outside of inline constraint [0, inf].');  scalar_tensor_1 = None
            return (full,)
```

Notice that the asserts are actually very odd:

```
            sym_size: Sym(i4) = torch.ops.aten.sym_size.int(full, 0)
            ge_1: Sym(i4 >= 0) = sym_size >= 0;  sym_size = None
            scalar_tensor_1: f32[] = torch.ops.aten.scalar_tensor.default(ge_1);  ge_1 = None
            _assert_async_1 = torch.ops.aten._assert_async.msg(scalar_tensor_1, 'full.shape[0] is outside of inline constraint [0, inf].');  scalar_tensor_1 = None
```

This is effectively creating a tensor *just* so that it can be sent to assert async. This is wasteful. We should just have a variant of assert that takes a scalar directly.

But I also question the premise that these asserts have to live in the graph. For example, in Inductor, it wouldn't be necessary to query `sym_size` on full to extract the integer; Inductor always has all shape env symbols in scope, and can just generate code to tests any needed invariants on these symbols. IMO, there's a decent case to be made that these should managed extralingually. Maybe this is the most convenient format for arbitrary export, but for Inductor I think we prefer something else.

cc @msaroufim @wconstab @bdhirsh @anijain2305 @aakhundov @tugsbayasgalan @gmagogsfm 

### Versions

main