{"record":{"id":"2af0b194e2cc86fb","repo":"sgl-project/sglang","slug":"mlx-call-multi-operation-must-return-a-non-empty-t","errorCode":null,"errorMessage":"mlx_call_multi operation must return a non-empty tuple or list of MLX arrays","messagePattern":"mlx_call_multi operation must return a non-empty tuple or list of MLX arrays","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"python/sglang/srt/utils/tensor_bridge.py","lineNumber":366,"sourceCode":"\n    borrowed: tuple[Any, ...] = tuple(\n        (\n            tensor.array\n            if isinstance(tensor, MlxTensorView)\n            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 = []","sourceCodeStart":348,"sourceCodeEnd":384,"githubUrl":"https://github.com/sgl-project/sglang/blob/0132848349585cfe6aae51c4941cbae872505f8a/python/sglang/srt/utils/tensor_bridge.py#L348-L384","documentation":"The operation callback passed to mlx_call_multi must return a non-empty tuple or list of results (single outputs belong to mlx_call). After invoking the callback, the wrapper checks the container type and emptiness before touching elements.","triggerScenarios":"A callback returning None, a single mx.array, an empty list [], or any non-tuple/list object.","commonSituations":"Reusing a single-output op in mlx_call_multi without wrapping its return, or an op with a conditional early `return` path returning None.","solutions":["Return a tuple: `return (result,)` from the callback","Use mlx_call for single-output operations","Ensure every code path in the callback returns the container"],"exampleFix":"# before\ndef op(a, b):\n    return a + b  # single array\nouts = mlx_call_multi(op, [a, b])\n# after\ndef op(a, b):\n    return (a + b, a - b)\nouts = mlx_call_multi(op, [a, b])","handlingStrategy":"validation","validationCode":"result = op(*inputs)\nassert isinstance(result, (tuple, list)) and len(result) > 0","typeGuard":"def is_valid_multi_result(r) -> bool:\n    return isinstance(r, (tuple, list)) and len(r) > 0","tryCatchPattern":null,"preventionTips":["Always return tuples from multi-output ops, even single-element ones","Use mlx_call for single-output operations"],"tags":["sglang","mlx","callback","return-type","contract"],"backgroundTag":"callback-contract-violation","analyzedSha":"0132848349585cfe6aae51c4941cbae872505f8a","analyzedAt":"2026-08-28T05:10:05.995Z","schemaVersion":2},"datasetVersion":"2026-08-28T06:17:29.519Z"}