{"record":{"id":"74ccff3ce2a881fb","repo":"sgl-project/sglang","slug":"mlx-call-multi-outputs-must-be-mlx-arrays","errorCode":null,"errorMessage":"mlx_call_multi outputs must be MLX arrays","messagePattern":"mlx_call_multi outputs must be MLX arrays","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"python/sglang/srt/utils/tensor_bridge.py","lineNumber":371,"sourceCode":"            else _torch_to_mlx(tensor.detach(), copy=False, synchronize=False)\n        )\n        for tensor in tensors\n    )\n\n    if target_device.type == \"cpu\" and any(\n        array.dtype == mx.float64 for array in borrowed\n    ):\n        with mx.stream(mx.cpu):\n            result = operation(*borrowed)\n    else:\n        result = operation(*borrowed)\n    if not isinstance(result, (tuple, list)) or not result:\n        raise TypeError(\n            \"mlx_call_multi operation must return a non-empty tuple or list of MLX arrays\"\n        )\n    arrays = tuple(result)\n    if any(not isinstance(array, mx.array) for array in arrays):\n        raise TypeError(\"mlx_call_multi outputs must be MLX arrays\")\n\n    # Prepare all outputs before crossing the one shared MLX evaluation\n    # boundary. This is the key difference from calling mlx_to_torch in a\n    # loop, which would fence/evaluate every result separately.\n    arrays = tuple(_prepare_mlx_export(array, target_device, mx) for array in arrays)\n    mx.eval(*arrays)\n\n    # DLPack cannot represent negative strides. Materialize all such outputs\n    # together so even this safety path has one additional evaluation boundary\n    # rather than one boundary per result.\n    negative = tuple(_has_negative_stride(array) for array in arrays)\n    if any(negative):\n        materialized = []\n        for array, needs_materialization in zip(arrays, negative):\n            if needs_materialization:\n                stream = mx.cpu if target_device.type == \"cpu\" else mx.gpu\n                array = mx.contiguous(array, stream=stream)\n            materialized.append(array)","sourceCodeStart":353,"sourceCodeEnd":389,"githubUrl":"https://github.com/sgl-project/sglang/blob/0132848349585cfe6aae51c4941cbae872505f8a/python/sglang/srt/utils/tensor_bridge.py#L353-L389","documentation":"After the container check, mlx_call_multi verifies every element is an mx.array; mixed Python scalars, torch tensors, or NumPy arrays in the returned sequence raise this TypeError before the shared evaluation boundary.","triggerScenarios":"A callback returning (mx.array, float) or (mx.array, torch.Tensor) — any non-mx.array element.","commonSituations":"Returning loss values or metadata alongside tensors from the op; forgetting mx.array(...) conversion for scalars.","solutions":["Wrap scalars: mx.array(loss_value)","Convert torch outputs back through torch_to_mlx or restructure to return only MLX arrays"],"exampleFix":"# before\ndef op(a):\n    return (a * 2, 0.5)  # 0.5 is a float\n# after\ndef op(a):\n    return (a * 2, mx.array(0.5))","handlingStrategy":"type-guard","validationCode":"assert all(hasattr(a, \"dtype\") and type(a).__module__.startswith(\"mlx\") for a in result)","typeGuard":"def all_mlx_arrays(seq) -> bool:\n    import mlx.core as mx\n    return all(isinstance(a, mx.array) for a in seq)","tryCatchPattern":null,"preventionTips":["Wrap scalars with mx.array(...) inside ops","Never mix torch tensors into MLX op returns"],"tags":["sglang","mlx","type-check","callback","return-type"],"backgroundTag":"callback-contract-violation","analyzedSha":"0132848349585cfe6aae51c4941cbae872505f8a","analyzedAt":"2026-08-28T05:10:05.995Z","schemaVersion":2},"datasetVersion":"2026-08-28T06:17:29.519Z"}