Use `xla::ifrt::Array::FullyReplicatedShard()` for materializing fully-replicated arrays
Use `xla::ifrt::Array::FullyReplicatedShard()` for materializing fully-replicated arrays

This allows us to avoid potentially expensive device array explosion when we know the array is fully replicated. To make repeated calls to `PyArray::FullyReplicatedShard()` as efficient as retrieving `py_arrays`, this CL also adds caching for `PyArray::FullyReplicatedShard()`.

Note: Most `FullyRepliciatedShard()` implementations today do not return an error if the array is actually not fully replicated and instead choose one shard, unlike `FetchSingleShard` which does have an (incomplete) shape-based check. This is acceptable because `_single_device_array_to_np_array` and `_copy_single_device_array_to_host_async` are internal APIs that are called only if JAX knows that the array is fully sharded, and its Python implementation does not have such a check either.
